preactjs / preact

⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOM.
https://preactjs.com
MIT License
36.79k stars 1.95k forks source link

Local function components don't properly #4014

Closed MicahZoltu closed 1 year ago

MicahZoltu commented 1 year ago

Describe the bug Local function components do not persist across re-renders.

To Reproduce

export function Outer() {
    useEffect(() => console.log('Outer (re)initialized'), [])
    const apple = useSignal(0)
    function Inner({apple2}: {apple2: Signal<number>}) {
        useEffect(() => console.log('Inner (re)initialized'), [])
        return <MyComponent apple={apple2}/>
    }
    return <div>
        <Inner apple2={apple}/>
        {/* <MyComponent apple={apple}/> */}
        <div>Apple: {apple.value}</div>
    </div>
}
// function Inner({apple2}: {apple2: Signal<number>}) {
//  useEffect(() => console.log('Inner (re)initialized'), [])
//  return <MyComponent apple={apple2}/>
// }
function MyComponent({apple}: {apple:Signal<number>}) {
    useEffect(() => console.log('MyComponent (re)initialized'), [])
    return <button onClick={() => apple.value = apple.peek() + 1}>Increment</button>
}

https://codesandbox.io/s/eager-paper-3zy0tl?file=/index.js

Steps to reproduce the behavior:

  1. Load the above code/component into a project.
  2. Open browser console.
  3. Click the Increment button repeatedly.
  4. Notice that Inner and MyComponent gets re-initialized every time you click the button.
  5. Move Inner function to a file-level function rather than a local function.
  6. Notice that this behavior no longer occurs and it correctly re-uses the component.
  7. Comment out Inner and instead inline the component directly into Outer.
  8. Notice that this behavior no longer occurs and it correctly re-uses the component.

Expected behavior Local function components are correctly re-used on each re-render.

Notes

marvinhagemeister commented 1 year ago

Yup, that's the correct and expected behavior. Creating component functions inside another component is a big no no, as this will create a fresh new function reference on every single render. When the framework compares the component during rendering it sees that the function reference has changed which means it's a different component. They just happen to have the same function body, but are completely different functions. You can check that in the console yourself:

const a = () => {};
const b = () => {};
console.log(a === b); // returns false

So yeah, this is the expected behavior with any framework that is based around diffing like virtual-dom based ones. You can fix this by not creating components definitions inside components.

MicahZoltu commented 1 year ago

Ah, I see. Thanks! I didn't realize two things:

  1. That preact was using function equality for tracking this stuff.
  2. That local named functions were recreated with each call (I thought only lambdas were transient like that).
    function a() {
    function b() {}
    return b
    }
    a() === a() // false; I thought this was true