vercel / next.js

The React Framework
https://nextjs.org
MIT License
126.78k stars 26.95k forks source link

private fields broken in specific case #70834

Open james-tindal opened 1 month ago

james-tindal commented 1 month ago

Link to the code that reproduces this issue

https://codesandbox.io/p/devbox/cool-yalow-zzkp58

To Reproduce

Click preview Wait for it to hydrate It throws an error: "attempted to get private field on non-instance"

Current vs. Expected behavior

Here is the same code running in react without Next.js: https://codesandbox.io/p/sandbox/tslrlg The getter does not throw and returns the value of the private field.

In the broken Next.js version, it renders fine on the server side, but when it rehydrates it throws. The bug only happens if all of these are true:

The following function formats do not trigger the error:

"use client";
import { useRef } from "react";
import { construct } from "./construct";

export default function Home() {
  const a = construct()
  const ref = useRef(a);
  const b = ref.current;

  return b.test;      // throws client-side
}
export const construct = () => new class {
  #test = 99
  get test() { return this.#test }
}

Provide environment information

It's running on CodeSandbox

Which area(s) are affected? (Select all that apply)

Runtime

Which stage(s) are affected? (Select all that apply)

next dev (local)

halilxibrahim commented 1 month ago

If I understand correctly, the problem may be This issue arises from how private fields are handled in Next.js. Private fields in JavaScript classes are defined with #, and they can only be accessed by the class's own instances. In Next.js, the server-side rendering (SSR) and client-side hydration processes can lead to problems with managing these fields.

In the provided code, an error occurs during hydration when accessing an instance of a class created with useRef or useMemo. Here are some potential solutions to address this issue:

Directly Use the Class Instance You can avoid the problem by directly holding the class instance instead of using useRef.

james-tindal commented 1 month ago

Thanks @halilxibrahim. That is what I ended up doing.

kiruthikpurpose commented 2 weeks ago

If I understand correctly, the problem may be This issue arises from how private fields are handled in Next.js. Private fields in JavaScript classes are defined with #, and they can only be accessed by the class's own instances. In Next.js, the server-side rendering (SSR) and client-side hydration processes can lead to problems with managing these fields.

In the provided code, an error occurs during hydration when accessing an instance of a class created with useRef or useMemo. Here are some potential solutions to address this issue:

Directly Use the Class Instance You can avoid the problem by directly holding the class instance instead of using useRef.

Smart approach!

james-tindal commented 2 weeks ago

I made a hook that ensures a single class instance per component instance and allows garbage collection.

const instances: Record<string, WeakRef<WeakKey>> = {}
function add(key: string, factory: () => any) {
  const oldInstance = instances[key]?.deref()
  if (oldInstance)
    return

  const newInstance = factory()
  instances[key] = new WeakRef(newInstance)
}
const get = (key: string) => instances[key]?.deref()

export const useInstance = <T extends WeakKey>(factory: () => T): T => {
  const id = useId()
  const initialise = () => add(id, factory)
  useMemo(initialise, [])
  useEffect(initialise, [])

  return get(id) as T
}