frejs / fre

:ghost: Tiny Concurrent UI library with Fiber.

Home Page:https://fre.deno.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

useState and useReducer dispatch unstable between renders

kavika-1 opened this issue · comments

Using a library that works in React/Preact, but not in fre. Upon debugging, saw this code:

  const [embla, setEmbla] = useState<EmblaCarouselType>()
  ...
  useEffect(() => {
  ... setEmbla(...)
  }, [..., setEmbla])

The useEffect gets triggered unexpectedly, which is any time the function component (re)renders. It appears to be due to setEmbla being assigned a new function, thus triggering the useEffect again. Understandable why the library writers did this, since rules of hooks requires adding anything used in the useEffect to be declared in the dependencies array.

Does it make sense to address this?

https://github.com/yisar/fre/blob/master/src/hook.ts#L31

https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.new.js#L1515

Maybe it's meaningful. I don't see that react has additional processing here. Can you write a demo use case? Let me see the specific situation first.

Just I am not familiar with fiber, so passing along... in React 17 old hooks looks like they use ReactCurrentDispatcher.current:
https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js#L24
https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js#L82

In ReactFiberHooks.new, further along there are several references to ReactCurrentDispatcher.current as well:
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.new.js#L2601

Maybe that helps? Otherwise, will post a contrived example below

Contrived example: Want to initialize a state in useEffect. However, will make setState a dependency of useEffect since supposedly that is how rules of hooks wants us to behave.

Running the fre version:

  • Console reports "function component rendering" 94 times (this number varies)
  • Upon clicking [+], State: 100 never changes, it gets reset to 100
    • But - the increment() function reports it's own value incrementing
  • If setState is removed from the dependency array, everything works fine

Running the react@17 version (comment out // fre line, and uncomment // react lines

  • works fine with or without setState in the dependency array
<!DOCTYPE html>
<html lang="en">

<body>
  <div id="test"></div>
</body>

<script type="module">

  // fre
  import { render, h, useState, useEffect } from 'https://unpkg.com/fre@2.4.4/dist/fre.js'

  // react
  // await import ('https://unpkg.com/react@17.0.2/umd/react.development.js')
  // await import ('https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js')
  // const { render } = ReactDOM;
  // const { createElement: h, useState, useEffect } = React;

  function Test() {

    console.log('function component is rendering')

    const [state, setState] = useState(0)

    useEffect(
      () => {
        setState(100)
      },
      [setState]
    )

    function increment() {
      setState(v => {
        console.log('increment to', v + 1)
        return v + 1
      })
    }

    return (
      h('div', {}, [
        h('p', {}, `State: ${state}`),
        h('button', { onClick: increment }, '+')
      ])
    )
  }

  const el = document.getElementById('test')
  render(h(Test, {}), el)

</script>

</html>

Screenshot when running fre - note the state does not increment in the DOM, but still does in setState...
image

Screenshot when running react@17 (or fre without setState in the useEffect deps)
image

Thank you. I fixed it. setState should only be initialized once.
Please update 2.4.5

I can confirm that the actual real component(s) now work as well. That's incredible turn-around time 👍 🥇 Thank you!