-
-
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
Replace shallowEqual with reference equality in useSelector #1288
Replace shallowEqual with reference equality in useSelector #1288
Conversation
Deploy preview for react-redux-docs ready! Built with commit 062d5fc |
I'm not sure I agree with this approach. It puts too much onus on the user to memoize, without giving them the tools to do that. You can't useMemo, because I feel like a better option is to make this customizable via an option, as was suggested here: #1252 (comment) |
Yeah, if at all I would suggest making this an option. |
@perrin4869 I'm just a passerby, but I'd like to know, how much |
Hm... I am still a bit confused.
If so, then keeping If the docs instead choose:
Then I am all for adding an option, but I think that this PR should be the default, and |
While people should be breaking up their While we can and should advise away from that style of usage, we can't prevent it and should use a reasonable default to prevent it from being a giant pain for most users. |
An alternative to using a local option would be to make it a global one, either via the provider or just statically in code, something like Btw, my main argument for keeping shallow equality as the default is not that it makes it easier to migrate from const { myLargeObject } = useSelector(state => ({ myLargeObject: mySelector(state) })) So with shallow equality as the default, both versions (returning an object or using multiple |
The problem is that the performance of this:
Is actually pretty bad compared with the performance of doing referential equality. Because with referential equality the only thing that happens is a straight comparison, nothing else. Compare the number of operations that take place with referential equality with the number of operations that will take place with the "trick" that you are suggesting:
All that is going to happen every single time that a dispatch takes place. The thing is that if you have an already memoized selector that returns a large object, you will be better off not using the trick that you are suggesting. Because without using the trick that you are suggesting, you will avoid the creation of superfluous objects at every Long story short: the trick that you are suggesting looks smart, but in reality it would only make performance worse if you already have a memoized selector. |
@josepot I think we are talking about different things. Let me outline my understanding of this issue from the beginning, so that you may point out where I am going wrong since I feel I must have fundamentally misunderstood something. As far as I understand the main motivation behind not wanting to do a shallow equality comparison is that people don't want to pay the price for it if the selected slice of state is an object with many keys or a large array. The OP's post also mentions that So, as you say, the shallow equality comparison does a reference equality check first. Therefore, if you are using a memoizing selector and don't use my workaround, then you won't feel the effects of the shallow comparison as long as the selected state doesn't change. In the cases where it does change, you will pay the price for the comparison. Now in regards to my suggestion it depends on how often the selected state changes. If it almost never changes, then you are probably fine with just sticking with the shallow comparison directly, since you rarely pay the full price for it. However, if your selected large array/object changes very often, then I suggest you use my workaround. The additional price you pay for those few operations you mention will then indeed be paid on each dispatch, but that price will be negligible in comparison to having to re-evaluate the selector in the first place. Lastly, this is once again an issue about performance where people are making lots of assumptions. As I mentioned in another thread, when I prototyped the hooks API I ran the benchmarks (which have some worst case scenarios for shallow equality) with the shallow and reference equality comparison and saw no noticeable difference. If you want to convince us this change is necessary, please show us the need with concrete numbers and examples instead of hypotheticals. |
I can't talk for others. My main reasons for not wanting to do a shallow equality comparison inside If const getShallowMemoizedSelector = selector => {
let latestResult;
return state => {
const result = selector(state);
if (shallowEqual(result, latestResult) {
return latestResult;
}
return (latestlResult = result);
}
}
const useObjectSelector = selector => {
const memoizedSelector = useMemo(
() => getShallowMemoizedSelector(selector),
[selector]
);
return useSelector(memoizedSelector);
} Which is correct and it doesn't leak anything. On the other hand if That's why I think that if
I hope it's now clear that my main concerns are not "performance", but correctness and not to leak an implementation detail. However, I don't understand your reasoning with this comment. IMO you should be the one showing with numbers that this performance optimization is actually worth it. Meaning, that you should provide the numbers that show that this perf-optimization is needed, not the other way around. @timdorr : Almost a month ago I also thought and proposed that it would be a good idea to have this as an option of Although, if this ends up becoming an option I think that the default option should be strict-equality 🙂 |
Still busy, still skimming the discussions vaguely, but I'm also starting to get a bit tired of the back and forth here. If people feel there are perf concerns, fork, benchmark, and PR. |
If you read my latest comment you will see that for some of us the main concern here is not performance. It's to avoid having an API that leaks an implementation detail and to have a hook that always works correctly. Also, if later we want to change this, it won't be as easy as:
If 7.1 gets published with the current implementation, then if we wanted to change this we would need a major version upgrade, because this change wouldn't be backwards compatible. I think that this is important. Please, take your time to better understand what all the different concerns are with the current implementation. |
@josepot : can you clarify what you mean by "leaking an implementation detail"? The equality approach used is going to matter regardless of what we choose. Folks will need to know that and write their code accordingly. |
Using shallow-compare is a performance optimization. Performance optimizations are implementation details. Implementation details shouldn't be leaked through the API, and the users of the API should not have to work around an implementation detail in order to make their selectors work properly. If However, that is not the case: A user can have a store that only contains serializable data. However, when deriving the state, they may want to compute some non-serializable data, like a Date, a Promise, a Map, a Set, etc. That is a very legit thing to do in selectors. If Since If we find users that experience perf issues because their selectors return objects, then we can help them fix those perf issues in many different ways:
That is, of course, if that performance issue actually exists, which we don't even know if that's the case. But I agree that's possible and even likely. However, that's something that can and should be solved outside of |
The "performance optimization" part is critical. One of the key goals of React-Redux from day 1 has been that "your own component only re-renders when needed". If we didn't take care of that, you'd end up with every React component in the app re-rendering on every dispatch, which would kill performance. And that's the whole point here. The goal of this equality check is to ensure that this individual subscription only causes a re-render when whatever value(s) it's returning have actually changed. I'm not ruling out strict equality as an option, I'm just saying that the way you're arguing about this (both terminology and insistence) isn't exactly selling it to me. In general, the things we need to consider here are:
I don't have hard answers for any of those yet, but those should be some of the deciding factors. |
Let me phrase this another way: Please show me some concrete cases where things break with the shallow-equality approach, and work "as expected" with strict equality. The |
I already did that in my previous comment when I shared with you this example, that it's based on a real case that you found on twitter. A Let me prepare a few Codesandboxes for you. Would that actually help my case? Because, quite honestly, I feel like giving up here. |
I am starting to come around to the reference equality side. The argument about selectors being able to return anything is compelling and something that I hadn't considered before. With I still see the arguments for the shallow comparison, but I am about 50/50 now. If we decide to go with reference equality, we just need to make sure to very clearly state in the docs that either multiple |
Sure, more examples would help. The flip side of this is that anyone who does want to return multiple values is going to have to self-memoize their selector. That may not be too difficult, but I would argue that returning an object full of values would be a lot more common than returning a Promise from a selector. It also would require anyone migrating from And, as @MrWolfZ pointed out, the workaround for wanting to return something else is trivial - just return the value you want in an object or array, and destructure the result. |
Btw, one additional thing to consider should be this: if we go with reference equality, it is easily possible that the users will use object selectors everywhere without noticing the performance issues, since everything will still work correctly (or they may notice the issues very late, causing them to have to make big refactorings). If we go with shallow equality, then yes, the cases pointed out by @josepot will fail, but that will be quite obvious since the component won't update when the user expects it to. The workaround would IMO still be what I proposed above, i.e. wrapping the return value with an object. That should work for promises, maps, and any other value. However, this would then also need to be mentioned in the docs. Sadly neither solution is perfect, so I am still at 50/50. |
Here's my two cents. I'm on the ref equality side. |
More concrete cases where things break with the shallow-equality approach, and work with strict equality:
shallowEqual(new Date(123456), new Date(654321)) // => true
shallowEqual(
new URL('https://github.com/reduxjs/react-redux/'),
new URL('https://github.com/reduxjs/react-redux/pull/1288')
) // => true
shallowEqual(
new URL('https://github.com/reduxjs/react-redux?foo=foo').searchParams,
new URL('https://github.com/reduxjs/react-redux/pull/1288?bar=bar').searchParams
) // => true
shallowEqual(new Set([1,2,3]), new Set([3, 2, 1])) // => true
shallowEqual(new Map([[1, 2], [2, 1]]), new Map([[1, 3], [2, 4]])) // => true
shallowEqual(Promise.resolve(true), Promise.resolve(false)) // => true I'm sure that I could write a much longer list, but I will leave it here for now. |
@josepot : thanks for that list. So, in general, it seems to boil down to "instances of classes", pretty much. I'm still not completely convinced, yet, but having that list does help as food for thought. Overall, it seems like the options are:
|
So here's an idea :
Thoughts? |
I see it in a slightly different way. IMO the React hooks API has been purposely designed so that if users want to move to hooks, they are in a way forced to break down the logic that they had in their classes into a lot smaller pieces. An example of that is how That's why I don't necessarily think that it's a bad thing that porting existing code to hooks "invites" users to break their containers down into smaller pieces. I think that you will agree with me that even before hooks, it was a best practice to have many "lean" containers as opposed of having a few "fat" containers. I think that in reality, the transition to hooks will be fairly easy for those that had "lean" containers and not so easy for those that had "fat" containers (which usually implies having lots of props). Just like the transition to hooks has been a walk in the park for those that were enhancing our components with tools like recompose, and it's going to be a nightmare for those who were writing tightly coupled classes with lots of logic in them... I don't think that As I was finishing to write this comment I saw your latest comment. I like a lot that suggestion. |
Export shallowEqual function
Hi @perrin4869 and @markerikson ! I really like @markerikson latest proposal. I think that it finds a very good balance between "correctness" and convenience. That's why I just opened the following PR in @perrin4869 repo, so that if @perrin4869 accepts it that commit will be added to this PR. If that makes things too complicated, no worries @perrin4869 just take my code and add your own commit. The reasons why I like @markerikson idea so much is because:
With this change it would be pretty easy for those users to fix those perf issues without making the refactor too tedious, just adding the second argument into the problematic
|
I'm with the second arg being for equality, particularly because it mirrors The vast majority of our users are dealing with serializable data in their stores and the selection of that data rarely goes through anything more than a simple field selection. So, referential equality is going to be the wrong choice for the majority of our users and will be a painful transition. This is evidenced by the relatively minimal usage of Again, I want to see this be an option; customizability is good. But I don't think it's doing anyone any favors to force "good form" on everyone when they're not actually doing anything wrong. |
Sure! In this specific case I mean that If the documentation states that the selector of
and its definition is: const result : any = useSelector(selector : Function) Then, I think that the correct behavior is that it gets updated whenever the selector returns a different value. From the user point of view it shouldn't matter if that type is a I think that defaulting the equality comparison function to something that doesn't work correctly for all cases (when that would be possible) implies sacrificing correctness for performance... And I think that there is no need for doing that. I still think that if we wanted to help users migrate from Would that increase the API surface area? Yes, it would. But so it does exporting If That would still be my preferred option. But I'm happy to compromise with a solution that at least allows for any value to work in the way that I consider to be correct. Also, I'm aware that a point could be made by saying that if the docs stated that the selectors of const result : anySerializableType = useSelector(selector : Function) 😅 PS: Thanks for asking for clarification. Regardless of whether you agree with my thoughts or not, that shows that you actually care about everyone's opinion. Even the opinions of those who are as annoying as I can be 😄. I really appreciate that. |
The issue isn't "serializability", per se - it's that most of the built-in JS class types don't have any instance fields, so multiple instances would be considered shallow equal. If I had two instances of a I generally see what you're trying to say about "correctness". I don't feel it's nearly as big a deal as you're making it out to be, but I do understand what you're getting at. For that matter, someone could also argue that even doing equality-based comparisons itself is over-opinionated - what if someone were updating a I do disagree that you could "use this hook to create any other selector hook". That's sort of the root of the issue. The With Given that we're having this debate over equality methods, I'd rather just make it configurable instead of exporting multiple hooks. The rest of the logic for subscriptions and everything is identical - there's no reason to duplicate that. It also opens up the door for someone to use |
Awright. Semi-executive decision: let's go merge in perrin4869#1 and go with the plan I suggested. I know @timdorr expressed a strong preference for shallow equality instead. That said, I'm willing to give the "ref equality + comparison func" approach a shot and see how it works out. |
Then they should go back to MVC where controllers can "poke" the models as much as they want 😄. I mean, I know that Redux is pretty unopinionated, but if you have chosen Redux is because you appreciate the benefits of working with an immutable state, and in JS what determines if an instance has changed is referential equality. There are "tricks" that can be made -in some instances- to determine if the new state is equivalent to the previous one, and in a FP paradigm if you can be certain that the next state is equivalent to the previous one, you can assume that nothing has changed, for sure! But those "tricks" only work in some cases.
The beauty of having the guarantee that you are working with pure functions is that you can enhance them, so it is possible to do that by enhancing the selector. Check this out, imagine that const selectorEnhancer = (selector, equalityFn) => {
let latestResult;
return state => {
const result = selector(state);
if (equalityFn(result, latestResult) {
return latestResult;
}
return (latestlResult = result);
}
}
const useSelector = (selector, equalityFn) => {
const enhancedSelector = useMemo(
() => equalityFn
? selectorEnhancer(selector, equalityFn)
: selector,
[selector, equalityFn]
);
return useStrictEqualsSelector(enhancedSelector);
}
Sure. That makes sense. |
Went ahead and pushed @josepot 's commit to @perrin4869 's branch rather than waiting for that sub-PR to be merged. Merging this in. |
Just to clarify, the proposition here is not either or. I want to make const useObjectSelector = selector => useSelector(selector, shallowEqual); The reason is that we are exporting Exporting |
Already merged :) I don't want to ship a hook that's 99% duplicate, and I can see use cases for making the equality configurable. I am throwing |
Yeah, thanks about that :)
I can too, I'm NOT saying to make it non configurable lol. Keep it configurable AND export a specific configuration. Just wanted to make sure it is not misunderstood Well I'm just a bit iffy about exporting |
Well, it's still an alpha :) We can tweak things further if so desired. |
Ok I'll send a PR tonight for your consideration then :)
…On Mon, May 20, 2019, 10:34 AM Mark Erikson ***@***.***> wrote:
Well, it's still an alpha :) We can tweak things further if so desired.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1288?email_source=notifications&email_token=ABMB27GLDPETVQOZQSZHUEDPWH52LA5CNFSM4HNWF4R2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVXPUKA#issuecomment-493812264>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABMB27EOALXYWJIQLSMQ6N3PWH52LANCNFSM4HNWF4RQ>
.
|
I'm late to the party, but strongly agree with the decision to default It might make sense to have |
* Replace shallowEqual with reference equality in useSelector * useSelector: Allow optional compararison function Export shallowEqual function
* Replace shallowEqual with reference equality in useSelector * useSelector: Allow optional compararison function Export shallowEqual function
…1288) * Replace shallowEqual with reference equality in useSelector * useSelector: Allow optional compararison function Export shallowEqual function
I thought I'd put this out there to get discussion going on this, before
7.1
final goes out, as I think this is a very important thing to iron out, and having a PR implementing the change will go a long way.As discussed in #1252 (comment), the argument for keeping
shallowEqual
seems to be that it is howconnect
currently works - well, as already stated in that thread,useSelector
is not a replacement toconnect
, and the hooks API have already deviated from the familiar HOC API already, for example, droppinguseAction
.I think that consumers of
useSelector
will be familiar withuseState
and will expect an API similar to that one - which deviated fromsetState
in the same way that this PR deviates fromconnect
. I think it will cause confusion for people when suddenly they see that theirconst username = useSelector(getUsername)
hook causes rerenders they aren't expecting, and will have to scourge through the docs to find the very awkward looking workaround:const { username } = useSelector(state => ({ username: getUsername(state) }))
.Additionally, the use of
shallowEqual
here seems to be a case of early optimization which could possibly be introduced in the future if the need arises.