cefn / watchable

Repo for @watchable/store and supporting packages.
MIT License
5 stars 1 forks source link

Improvements to store-react useSelected #43

Closed cefn closed 9 months ago

cefn commented 9 months ago

Liveness

In this reworked implementation, the selected value is always 'derived' from props inline in the render. This means the render has the very latest value of selected.

Previously the updating of selected was handled via a setState that was wired up in a useEffect hook which put state propagation outside the render.

In the worst circumstances, (for example if an earlier-subscribed watcher had already triggered a synchronous render before later watchers executed), a later watcher's state changes would require a further render, and the state wouldn't be updated until the later render.

The same effect occurred when the selector was defined as a callback dependent on some component props - rather than immediately affecting state, the change of selector would simply invalidate the useEffect meaning the eventual state change would only propagate after the render.

With this new implementation, the render itself recomputes the selected. If the render has already run, it will have already derived selected state, and synchronized it to the React tree. When notified, since it shares a memoized version of the selector the watcher detects there is no more work to do, and doesn't trigger a render. This means state changes propagate quicker and more predictably, and fewer renders are needed for these edge cases.

'Caching' computed values

useSelected didn't previously wrap your selector function in a memoizing wrapper (guaranteeing the same return given the same state).

Memoization is a subtle art for less confident React developers. Since memoization of a single arg function is very lightweight and easy to implement, and is a requirement for the change to state propagation above, this is now added by default.

Now if your selector and the state are identical since the last execution, then useSelected will return the previous value.

Crucially this eliminates the case where derived computed values give a new unique value on every render, even when the underlying state hadn't changed.

For example, if you wrote a hook like this, it would force any component that consumed the summary value to re-render, even when nothing had changed, because it constructs a new object every time.

type State = Immutable<{
  charges:number[]
}>

function summarySelector(state:State){
  const { charges } = state;
  return {
       count: charges.length, 
       total: charges.reduce((acc, charge) => acc + charge, 0)
  } as const;
}

function useSummary(store: Store<State>){
  return useSelected(store, summarySelector)
}

With the new implementation, summarySelector is only re-executed if it or the state has actually changed. Until then, the first summary object is used.