v9.4.0-alpha.1 #2403
markerikson
started this conversation in
General
v9.4.0-alpha.1
#2403
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
This alpha release fixes a bug where
useSignalSelectorstopped updating when a memoized Reselect selector was shared between components or called with the same state twice, changes how array methods called on tracked state return their elements, and adds a per-evaluation read cache plus a dev-mode warning for selectors that repeatedly read the same field in a loop. The stockProvideranduseSelectorare unchanged.This is still an alpha. Please try it out and give us feedback, especially on real apps using Reselect and entity adapters. The alpha.0 notes cover what
useSignalSelectoris and how to opt in.Changelog
Shared and memoized selectors now update correctly
After the alpha.0 release, we got feedback asking about two cases: reusing the same memoized selector instance across components, and conditional logic inside of a selector altering what fields it reads.
In alpha.0, every
useSignalSelectorevaluating against the same state object received the same tracking proxy. Reselect'sargsMemoize(theweakMapMemoizedefault, andlruMemoize) keys its cache on argument identity, so the second evaluation with that proxy was a cache hit: the input selectors never ran, no state properties were read, and the hook recorded zero dependencies. That hook then never re-rendered.This affected more than the reported case of two components sharing one
createSelector. A single component with an inline wrapper (useSignalSelector(s => selectSummary(s).total)), a parametrized selector (selectScaled(s, factor)), nested memoized inputs, and components mounted after the cache was warm all went deaf after their first update.Every tracked evaluation now gets a fresh root proxy. The consequence is that
argsMemoizemisses on every tracked run and the input selectors execute each time. The result function still memoizes on the input values, so derived objects keep stable identities and are shared across components as before. Hand-rolled caches that never read state (if (cached) return cached) still cannot be tracked, but those are equally stale underuseSelector.We also reviewed conditional selectors (
s => s.toggle ? s.foo : s.bar). Turns out those already worked :) Dependencies are re-recorded from scratch on every evaluation, so switching branches drops the old branch and picks up the new one.Array methods return raw elements with identity dependencies
Once we fixed the cache bug and re-ran the benchmarks, we saw that the alpha.0 benchmark results were flawed. In particular, the
derived-selectorsbenchmark had appeared to be a significant perf improvement over stockuseSelector. However, with the cache fix in place, the correct behavior had more overhead. More hooks actually received updates, and result functions that iterated arrays of tracking proxies (selectAll, then.filterover the result) paid one proxy trap and one dependency registration per element read, per hook, per dispatch. Inside a Reselect result function that precision buys nothing, since the function re-runs on input identity anyway.To address this,
find,findLast,filter,slice, and nowmapreturn raw state objects instead of proxies, each with a single identity dependency on the element's path (items.{id:42}for arrays with a key field,items.3otherwise). Because state is immutable, an identity dependency never misses a change to that element; it can only over-run when a field you did not read changes. Fields read off a returned element are not tracked separately. Direct index access (state.items[0].name) keeps field-level precision.For
map, callbacks that return the element itself, a primitive, or a proxy obtained elsewhere (ids.map(id => entities[id])) get precise dependencies. Callbacks that construct new objects fall back to one dependency on the whole array.forEach,reduce, andflatMapare still not overridden and depend on the whole array.One side effect: elements returned from these methods compare correctly with
===against references held outside the selector, withoutunwrap().Repeated reads are memoized, with a dev warning
A selector like
comments.filter(c => c.postId === post.id)readspost.idonce per comment. Whenpostis a tracking proxy, each read is a trap call. Within one evaluation that read is idempotent: the value cannot change and the dependency is already linked. Each proxy now remembers its last primitive read for the current evaluation and returns it directly on repeat.In development, a selector that re-reads the same field 100+ times in a single evaluation logs a warning once, naming the path and suggesting hoisting the value into a local before the loop. Hoisting is still cheaper than the memo, but the memo recovers most of the difference without code changes.
Performance
Overall, the branch is still a significant perf win across most of our benchmark scenarios, and it now correctly handles additional common usage scenarios that we'd expect to see in real app code.
10 s per scenario, this release vs 9.3.0, Chrome,
react-dom/profiling. "Script" is total scripting time; "blocked" is main-thread time insidedispatch().Render counts match 9.3.0 within noise on every scenario.
The alpha.0 notes reported
derived-selectorsat -87%. That number was wrong: 150 of the 200 hooks in that scenario were affected by the cache bug and never re-rendered after their first update. With the fix, every hook re-runs its input selectors through the tracking proxy on every relevant dispatch, and that is the cost shown.one-component-many-slicesimproved from +36% in alpha.0 to +19%.Bundle cost is unchanged from alpha.0: zero for apps that do not import
useSignalSelector, about +7 kB min+gz for apps that do.Docs
The
useSignalSelectorAPI page now covers array-method return values, the Reselect interaction, and the repeated-read behavior.What's Changed
Full Changelog: v9.4.0-alpha.0...v9.4.0-alpha.1
This discussion was created from the release v9.4.0-alpha.1.
All reactions