Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to store-react useSelected #43

Merged
merged 2 commits into from
Nov 7, 2023
Merged

Improvements to store-react useSelected #43

merged 2 commits into from
Nov 7, 2023

Conversation

cefn
Copy link
Owner

@cefn cefn commented Nov 5, 2023

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.

@cefn cefn changed the title Improve useSelected Improvements to store-react useSelected Nov 6, 2023
@cefn cefn force-pushed the improve-useSelected branch 3 times, most recently from e2af731 to 3db80a8 Compare November 7, 2023 13:52
@cefn cefn merged commit 5ed58fb into main Nov 7, 2023
2 checks passed
@cefn cefn deleted the improve-useSelected branch December 2, 2023 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant