Use a SolidJS store to make selectors rerun only when needed, automatically #2043
Replies: 2 comments 5 replies
-
This is certainly an idea I'd like to experiment with in general :) Actually just talked with some folks from Slack in the last week who are seeing 10-20K selectors running on each dispatch, and that itself is a sizeable bottleneck. I do have some concerns with the example implementation, though. My most immediate concern with using Solid here is that just importing and using those methods is 10.5K min / 4.5K min+gz. Second, I don't think we can realistically do anything that involves changing |
Beta Was this translation helpful? Give feedback.
-
Right, it's definitely only meant as a proof of concept for now, but I think it's very interesting to compare the approaches of Redux and Solid. I think that the main difference can be illustrated pretty nicely using the concept of memoization. Let's say you have a pure function Redux' approach: Assume the argument to be immutable. Now it's easy to memoize, because you know that if the argument didn’t change, the result didn’t change either. You can implement such a memoization function using WeakMaps for instance: const memoizeImmutable = <T extends object, U>(fn: (t: T) => U) => {
const cache = new WeakMap<T, U>();
return (t: T): U => {
if (!cache.has(t)) {
cache.set(t, fn(t));
}
return cache.get(t);
}
} It basically caches the result of Solid's approach: Assume the argument to be a Solid store, which means that it is wrapped in a proxy in such a way that every time it is modified, its subscribers will be notified. This is very different from the case above, because the argument is now typically always the same reference, being mutated in place. Nevertheless, it is easy to memoize as well because you can simply subscribe to changes and then you will be notified of every change relevant to the part you subscribed to, so you can keep the memoized value up to date. Here is again how to implement it using WeakMaps: const memoizeSolidStore = <T extends object, U>(fn: (t: T) => U) => {
const cache = new WeakMap<T, () => U>();
return (t: T): U => {
if (!cache.has(t)) {
cache.set(t, createRoot(() => createMemo(() => fn(t))));
}
return cache.get(t)();
}
} This time we do not store results in the cache (as the same argument (w.r.t. referential equality) may give different results), but instead we store a reactive value, which is simply a function Both approaches accomplish exactly the same thing: being able to mutate a value using imperative logic, but in a way that still makes it cheap to know whether some specific part of that value actually changed.
But out of those two approaches, it seems like the Redux approach has more drawbacks, in particular it requires calling all selectors every time, and it is also potentially heavy on the GC as it creates a lot of new objects. The Solid approach, on the other hand, does not require calling irrelevant selectors (as relevant selectors will be notified directly from the state-changing logic), and it does not require creating new objects (it just mutates the existing ones). One potential drawback of the Solid approach is that
The reason I'm changing I'm still wondering whether there is a way to implement this idea simply as a store enhancer. Maybe it is possible to only change |
Beta Was this translation helpful? Give feedback.
-
I've been thinking about Redux performance lately, and in the type of apps I am building (large and complex UI, large and complex state, and a lot of places where dragging something with the mouse dispatches an action at every
mousemove
), selectors seem to affect performance significantly, even though I spent a lot of time and thought on memoization. It is also difficult to get memoization right, as the various discussions in the Reselect repository attest.But more precisely, I feel like the fact that every single selector reruns at every single state change is a performance bottleneck that is hard to overcome when an app gets big enough (also, it makes memoization critical). If you make small components each selecting their own data (which I believe is the recommended way to do, as it reduces the cost of React rerenders), you can relatively easily get thousands or tens of thousands of selectors that need to rerun at every frame, so even if they are all very fast/memoized, it will still take a non-negligible amount of time.
After reading more about SolidJS, I’m starting to really like the idea of signals and of reactive state, and it occurred to me that we could actually reuse Solid's stores and effects (but keep React for rendering) to get a pretty different implementation of React-Redux, with essentially the same API it has now, but with selectors that would rerun only when needed, and without having to memoize anything explicitly.
The idea is:
dispatch
, use the store'ssetState
together with Solid'sproduce
function, which means that you simply need to give a function that "mutates" the draft passed as an argument, exactly like we do today with Immer/RTKuseSelector
, create a Solid effect that applies the selector to the global state and store the result in a regular ReactuseState
. An effect in Solid is a reactive function that remember exactly which parts of the global state were accessed and will rerun automatically every time one of those parts gets modified by a dispatched action.I wonder if someone already explored a similar approach? What do you think about it? Are there some drawbacks that I am not thinking about?
Note that while it is also using proxies, this is pretty different from the ideas of
proxy-memoize
. If I understand it correctly,proxy-memoize
"only" intends to make selectors faster by memoizing them automatically, but the resulting memoized selectors still need to run after every dispatched action (as this is handled byreact-redux
). In contrast, what I am proposing would skip running irrelevant selectors entirely, so there would be absolutely no performance penalty in having many selectors that access unchanged parts of the state. And just likeproxy-memoize
, it would also simplify memoization considerably as you mostly wouldn’t need to think about it at all.Here is a proof of concept, a drop-in replacement for basic versions of
createReducer
,configureStore
,Provider
,useDispatch
, anduseSelector
(and you can keep usingcreateAction
from RTK). It is less than 100 lines of codes (including comments) and I tested it in a toy todo-list example. It works the way is described above, for instance toggling a todo item only reruns the selectors that actually look as thecompleted
property of the given todo item, and the other selectors do not rerun at all.Beta Was this translation helpful? Give feedback.
All reactions