-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Use Proxy-based selectors #1653
Comments
Does useTrackedState in #1503 fall into any of the three? |
@dai-shi - it is the first one. |
Thanks for putting this writeup together. Lemme try to come at the question for a different angle: What pain points of Reselect can be addressed by using a proxy-based selector approach, if any? My thought process here is that perhaps the right place to start is by finding some scenarios where Reselect is a pain to work with, and proxy-based selectors solve those pain points. We can then look at maybe including something in RTK as a stepping stone away from Reselect, as well as revisiting the |
Pain points of Reselect
Cache sizeProxies are not able to help here. There are ways to resolve the problem, but they are not bound to proxies. MemoizationTrackedSelector can check that It's again not bound to proxies or reselect, but only to how you write your code/selectors. There is always a way to produce a perfect memoization using reselect only. In other words proxies are not going to magically solve all the problems, but they might lower the bar and improve dev experience and target product quality in many different ways. |
So, proxy-memoize v0.2 supports nesting and should help lowering the bar. Are there any examples that I can tackle applying the method?
pretty much this. |
Here's a few libs that specifically try to deal with weaknesses in Reselect:
Related:
I'd say those are good starting points for comparisons. |
Thanks. So, I modified @josepot 's example in redux-views. https://codesandbox.io/s/spring-star-1yrqm?file=/src/proxy-memoize.js I noticed proxy-memoize would require useMemo/useCallback for parametric selectors. |
@dai-shi okay, yeah, the actual behavior-wise, is that ultimately just acting as a cache with size > 1? tbh the |
oops, I was trying to emulate the useCallback behavior in the example.
uh, no. cache size = 1, but memoization is based on state and a prop (carId) separately.
Ha ha, I guess we should create react-redux examples which are common to use useCallback. |
@dai-shi oooo, yeah, I forgot that there was another example file there. Hey, neat, it's got a Out of curiosity, is the separate variable read like |
Yeah, but that means memory leaks, and it does not solve the issue technically.
Yes. proxy-memoize has a mix of WeakMap cache and Array (with
|
Okay, so here's a use case I'm not clear on yet. Let's say I've got: const selectTodoDescriptions = createSelector(
state => state.todos,
todos => todos.map(todo => todo.text)
) Now I Is |
This is rather easy, because we don't even need nested memoize. const selectTodoDescriptions = memoize((state) =>
state.todos.map((todo) => todo.text)
); In this case, the tracking info is something like: Example: https://codesandbox.io/s/quirky-austin-yxnts?file=/src/index.js |
👀 👀 👀 💥 okay, see, THAT right there is an immediate win over Reselect! And the fact that this has a |
👍 👍 👍
Yeah, that's correct. But, if it's used in react-redux, my suggestion (and that's why I used const selectTodoDescriptions = memoize((state) =>
state.todos.map((todo) => todo.text)
);
const Component = ({ todoId }) => {
const selector = useCallback(memoize((state) => {
const descriptions = selectTodoDescriptions(state);
// do something with todoId
return some_results;
}), [todoId])
// ...
}; (btw, react-hooks eslint rules can't handle this pattern, so we may prefer useMemo to useCallback.) |
🤔 what if one can provide a |
@theKashey not quite sure what you're suggesting there or how it might work, tbh. |
So we are here are trying to resolve a few underlaying issues:
Could be a solution for 2 and 3. Just provide cache side for a selector // please imagine that selectTodoDescriptions selects something based on props
// ie needs more that 1 cache size.
const selectTodoDescriptions = createSelector(
selector,
combiner,
500, /*or something*/
) Well, that would not work as cache side is not a constant, but a slight change can ease const selectTodoDescriptions = createSelector(
selector,
combiner,
state => /*🤷♂️*/ state.todos.length, // this is how many cache lines you need.
) Proxy based selectors, which uses const getUsersByLibrary = createCachedSelector(
// inputSelectors
getUsers,
getLibraryId,
// resultFunc
(users, libraryId) => expensiveComputation(users, libraryId),
)(
// Use "libraryName" as cacheKey
(_state_, libraryName) => libraryName
); The problem here is const getUsersByLibrary = keyedMemoize(
// inputSelectors
getUsers,
getLibraryId,
// resultFunc
(users, libraryId) => expensiveComputation(users, libraryId),
)(
/* cache size, can be >1, but still <500 */
10,
/* "key object", a weakmappable target to store cache in */
(_state_, {libraryName}) => _state_.libraries[libraryName],
); Now cache in bound to data, not React Components, which is 👍(data it source of truth) and 👎(actually we need memoization for UI) in the same time. const selector = useCallback(memoize((state) => {
const descriptions = selectTodoDescriptions(state);
// do something with todoId
return some_results;
}), [todoId])
⬇️⬇️⬇️⬇️
const selector = keyedMemoize((state) => {
const descriptions = selectTodoDescriptions(state);
// do something with todoId
return some_results;
}, {
cacheSize: 1,
key: (state, {todoId}) => state.todos[todoId],
}); Now key-ed proxy memoized selector can be extracted out of React Component 😎 |
Some interesting thoughts and examples there. The "keyed" example looks an awful lot like what I remember seeing from So, what would need to be added to |
This looks interesting. If we are sure the key is a stable object, we should be able to use WeakMap to cache memoized functions? import memoize from 'proxy-memoize';
const memoizedFns = new WeakMap()
const keyedMemoize = (fn, key) => {
if (!memoizedFns.has(key)) {
memoizedFns.set(key, memoize(fn));
}
return memoizedFns.get(key)
}; |
Okay, here's another weird edge case I'm curious about. Reselect selectors and Immer's Proxy-wrapped draft states don't get along well - the selectors think the Proxy ref is the same, so they don't recalculate results correctly. Over in reduxjs/redux-toolkit#815 , we're planning on adding a What happens with |
The current proxy-memoize is designed for immutable state model, so it doesn't help. If you pass a ref-equal draft, you get the cached result. This is to get better performance and for cache size issue. We could add an option to disable the immutability optimization and always compare all affected properties. But, I'm not sure how useful it would be. const memoizedFn = memoize(fn, { mutable: true }) // not yet implemented |
Yeah, I figured that was probably the case. I suppose it might be nice to have that as an option, but dunno how much more complex it makes the internal implementation. |
"Immutable Proxies" are reflecting the "immutable object"(State) underneath and change when that object changes. The same is about 👉 That is the game rules And when you are changing your state in reducer, and passing an "incomplete" state to selectors - you are bypassing these rules.
The missing puzzle piece here is a universal "proxy marker"(a Symbol), to indicate to any underlaying logic - "I am a Proxy, transform me if you need". This is how And look like @dai-shi is the best person to introduce such marker to the Proxy-based ecosystem. Might be one will need more than one marker, right now I can think only about "acces to the underlaying state"/"immutable" one. |
Something like
(but they would work slightly differently... also |
Something like it 😉. Actually, it's not quite about unwrapping, but closer to current - here is the object you should work with instead of me, please use it. |
Right, that's what I assumed.
It turns out that this is not trivial. It essentially needs to copy the object (at least partially) when it compares. |
so lemme ask this: how "done" / "ready" is
I just wrote up a big comment in Reactiflux about the behavior of Reselect vs proxy-memoize, so I'm definitely interested in trying to start advertising proxy-memoize as a recommended option - at a minimum on my blog, and potentially in the Redux docs shortly after that. I'd just like to know that it's sufficiently stable before I do so. |
Apart from selectors with props and cache size discussion, which would still be controversial, the basic feature is complete, I'd say.
Known edge cases I can think of is when a selector returns something that we can't
We should recommend a selector to return primitive values and plain objects/arrays (nesting is ok.)
The major concern of mine is not many people are using it and there can be unknown edge cases.
Would be super nice. So, I'd say it's ready as I covered all what I have noticed. |
Cool. Tell you what - could you update the readme with that sort of info on status and workarounds, and add some additional docs examples based on our discussions in this thread? In particular, I think some comparisons of some typical Reselect use cases and how they'd look with I'm going to find time over the Christmas break to update my existing "Reselect selectors" blog post to cover |
There is only one big problem with proxy-based solutions - they wrap the original object.
There is also a few more "good to have"s, which will require some changes from redux side:
|
yeah, I can see a use case for wanting to occasionally debug values inside of selectors. This goes back to the "unwrapping' discussion we had earlier. @dai-shi , is there an easy way to implement a version of @theKashey yeah, I know you've been poking at the idea of double-rendering |
@markerikson would you be able to link to this comment for myself and other travellers? |
This would be technically possible for proxy-memoize as it knows when it's finished. The implementation might not be trivial, though.
This should be easy as it already has an upstream api. A small hesitation of mine is not being sure how to export it as an api of proxy-memoize and how to document it. How would a user use such a util function to unwrap a proxy? |
@nathggns as I said, I'm going to be updating my existing blog post at https://blog.isquaredsoftware.com/2017/12/idiomatic-redux-using-reselect-selectors/ with that additional material over the break, as well as trying to add a couple docs pages based on that post. |
@dai-shi I would assume that it'd be similar to what you do with const todosSlice = createSlice({
name,
initialState,
reducers: {
todoToggled(state, action) {
const todo = state.find(t => t.id === action.payload);
// temporary code while I'm debugging
console.log(current(todo));
t.completed = !t.completed;
}
}
}) so, hypothetically: const selectScore= memoize(state => {
const intermediateResult = heavyComputation(state.a + state.b);
// temporary code while I'm debugging
console.log(unwrap(state.c));
return {
score: intermediateResult,
createdAt: Date.now(),
}
); |
@markerikson For a simple // temporary code while I'm debugging
console.log(unwrap(state.c)); You probably mean |
actually, yeah, the "[[Handler]]" stuff is part of what I'd want to hide. That's confusing for people. I think I was assuming that |
Yes, that's correct. Let me remind this just in case, as it's important: |
Hmm. Okay, yeah, that might be another gotcha to note. |
@markerikson
I did what I could. I'd need someone to help on this to make them better... |
Yeah, I'll find time in the next few days to play with this and help update the examples. |
This is more related with #1503, but as this thread is more active and it is slightly related, let me post here. import { useSelector } from 'react-redux';
import { createTrackedSelector } from 'react-tracked';
const useTrackedSelector = createTrackedSelector(useSelector); Now, this works pretty similar to useSelector + proxy-memoize. const getTotal = memoize(state => ({ total: state.a + state.b }));
// in component
const { total } = useSelector(getTotal);
// equivalent version
const state = useTrackedSelector();
const total = state.a + state.b; The major difference is useTrackedSelector returns a proxy, but memoized function unwraps proxies on return. The useTrackedSelector here is equivalent to useTrackedState in #1503. I just name it differently to avoid confusion. For working examples, check the example code and codesandbox at: https://react-tracked.js.org/docs/tutorial-redux-01 |
Sorta related, either of you ever seen https://github.com/pzuraq/tracked-redux ? |
I'm not familiar with Ember ecosystem at all, but it looks like it is for |
Yeah, it's definitely built on Glimmer's auto-tracking system, but my (vague) understanding is that that tracking system is UI-agnostic in much the same way Immer is (or Vue 3's new reactivity system). |
If I understand it correctly, mobx, vue, glimmer all base on mutable state. // mutable state
const state = { a: 1 };
state.a // mark as used (pseudo code)
state.a += 1
// ".a" is still marked as used.
// immutable state
let state = { a: 1 }
state.a // mark as used (pseudo code)
state = { ...state, a: state.a + 1 }
// the used mark is gone for the new state. I believe this behavior is important for hooks composability and Concurrent Mode. |
Interesting. So in terms of "tracking" mutable and immutable data structures differ more on boundaries and time. Like there are no boundaries/generations with mutable ones. From another point of view - it might be an interesting task to transfer usage information from one state to another. Is there any use case when it would be needed? "Usage flakes" are when more than one selector are assessing state, and at least one of them does "cache hit" and NOT accessing nested keys. That results the second run of memoized selector to produce might be the same value, but different usage information, which changes the way tracking works and alters the third run, when proxy would consider higher values as "important". I have a feeling that @dai-shi tackles this somehow, like I've seen the code somewhere. |
Yeah, that was really tricky with nested memoized selectors. |
Hiya, folks. At this point, I don't think we're likely to add a specific new proxy-based selector API to React-Redux. We do have a mention of I'm going to go ahead and close this. |
I reckon sooner or later proxies will make their way to the "read" part, as immer made it's way to the "write" part. |
Based on:
Proxy based solutions were floating around for a while, and actually actively used inside(MobX) and outside of React ecosystem(especially in Vue and Immer). I am opening this discussion to resolve status quo and add some momentum to Redux + Proxies investigations.
Overall proxies are capable to solve 3 objective:
proxy-memoize
orredux-tracked
What proxies can solve:
What proxies cannot solve:
What proxies can break:
state
with a Proxy - one will useProxy
in all "frontend" code. In the same time at reducers/middleware side it will be juststate
. (Only once) I had a problem with "equal reference" memoization working differently inuseSelector
andsaga
. It's very edge-case-y, but having state sometimes wrapped, and sometimes not can break stuff.Actionable items:
cc @markerikson , @dai-shi
The text was updated successfully, but these errors were encountered: