KidkArolis / tiny-atom

Pragmatic state management.
MIT License
116 stars 10 forks source link

Remove `sync` option, defaulting to `sync: true`, to fix issue with child component seeing newer atom state before parent #128

Closed tjenkinson closed 10 months ago

tjenkinson commented 10 months ago

React batches state updates, so multiple rerender state updates in the same frame won't trigger multiple rerenders. There will still only be one render, and it will start from the highest component that saw the state change and work down.

The problem with sync: false is that other state changes in a component can cause it to rerender, before the RAF callback triggers the state update due to an atom change. The early rerenders would still see the latest atom state though, which means child components may see a newer version of an atom state before a parent.

How does it break?

With the following

function Child() {
  const { user } = useSelector((state) => {
      return { user: state.user }
  });

  // `user` should exist at this point given `Parent` does not render this component if it's missing

  const [counter, setCounter] = useState(0);
  return (
    <div>
       <p>User id: {user.id}</p>
       <button onClick={() => setCounter((count) => count + 1}>{counter}</button>
    </div>
  );
}

function Parent() {
  const { user } = useSelector((state) => {
      return { user: state.user }
  });

  return user ? <Child /> : <div>No user</div>;
}

1.user is already set in the atom

  1. App renders Parent containing Child
  2. user is set to null in the atom
  3. A RAF call is scheduled for each component, preparing to trigger the rerender state update
  4. The user clicks the button resulting in setCounter being called
  5. The call stack ends, and react triggers a render of Child. It doesn't render Parent because no state changed above Child
  6. 💥 Error accessing id on user given user is null. useSelector always returns the live atom state

Note that requestAnimationFrame calls are usually paused whilst a tab is in the background, so it's more likely for other state updates to happen in components during that time.

If instead of the RAF call the rerender state was updated at that point, react would see the rerender state change for both components, and then trigger a single render from Parent down.

KidkArolis commented 10 months ago

A bit of a shame to not be able to batch requests anymore, that was one of the key differentiators of tiny-atom, but as you've clearly laid out - it can be buggy indeed! And React is now doing batching natively in more situations now. I'll cut a new breaking release.