-
Notifications
You must be signed in to change notification settings - Fork 674
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
Selectors return shared mutable values #737
Comments
I don't think this makes sense for reselect to be opinionated about, it just returns what you return. If you want to make sure it's readonly, use |
I understand the desire for reselect to not be overly opinionated. The inconsistency between the props in our app is due to our use of But the problem is that reselect is responsible for the memoized version of the object. If it were just returning the result of the selector function that would be one thing, but because this memoized value is shared between multiple usages of the selector it means that Reselect makes it shared mutable state. It's not obvious that one would have to add Or alternatively – should this be a PR on |
I'm not sure I understand what the actual complaint or "bug" is here. Is it the general principle that "many calls to the same memoized function return the same reference", and thus an accidental mutation of that reference in one place could cause a problem elsewhere? Is it that "you shouldn't mutate a returned reference, but the types aren't |
@markerikson Yes, sorry, in truth this is not really a reselect bug. Perhaps this would have been better phrased in the form of a suggestion. It sure seemed like a bug when I hit it! The inconsistency factor is due to the combination of this library and I think your first two principles are the thing. One shouldn't really mutate shared references because an accidental mutation could cause a problem, and a change to The shared reference being mutable is unexpected, and it's not reactive so you don't find out right away. It makes sense once you think about it, but it's an easy mistake to make; we had code that called I can't think of a downside to immutable selector returns (other than the admittedly huge amount of churn that comes with a breaking change). I suspect most users of We could Once I implemented this patch locally, many of our inconsistent props are consistent, our selectors are purer, and it feels much better to have all selected data immutable. It was a slightly tedious but rewarding refactor. An alternative would be to make a wrapper called something like |
I'm confused, where's the inconsistency? RTK doesn't wrap your state in |
RTK by default uses Immer's autoFreeze option, which prevents mistakes by freezing your objects which come out of the redux state, preventing accidental mutations in one component to affect the whole state. I think this would be a great change that would prevent lots of bugs, and I can see at least 2 ways to go about it (Possibly both):
An aside but I've just uncovered a similar bug to what @elliottkember did, and the timing is eerie, it was also caused by Edited to add: |
For anyone stumbling on this, I've realized you can manually implement the autofreeze behaviour centrally without patching reselect by taking one of the existing memoizer functions, and calling deepFreeze on the selector output before saving it to cache. @markerikson if there is appetite for integrating this safety feature directly, I'd love to create a PR |
@akaltar that would work - here's a quick util to make it easy to wrap any memoizer: import { lruMemoize, UnknownMemoizer } from 'reselect'
import { freeze } from 'immer'
function makeFreezingMemoizer<Memoizer extends UnknownMemoizer>(
memoizer: Memoizer
) {
return function wrapperMemoizer(...args) {
const selector = memoizer(...args)
return Object.assign(function wrapper(...args: any[]) {
return freeze(selector(...args), true)
}, selector)
} as Memoizer
}
// for example
const freezingLruMemoize = makeFreezingMemoizer(lruMemoize) |
Hi there!
I think I've uncovered a typing bug with this library, where selectors return shared mutable copies of their datasets.
Basically, if you return Immutable state directly from redux the result is immutable (the store can't be mutated, which is good). But if you return a mutable version of that array (for example, filtering store data) a mutable copy is returned.
This is problematic because that mutable copy is memoized and then used for any other usages of that selector. So one component can break the state of all the components using that selector with the same inputs!
Here is a reproduction sandbox: https://codesandbox.io/p/sandbox/amazing-curran-cfvj53?file=%2Fsrc%2FApp.js%3A16%2C32
We were able to make a patch fix on our end that resolved this issue quite nicely for us, by changing
Result
toImmutable<Result>
.Other than the fact that it's a breaking change requiring a lot of
readonly
additions to codebases, I don't think there is any inherent risk here as these selector return values should not be modified. We found that a lot of our code had inconsistent types, depending on whether their selectors returned immutable values straight from the store, or filtered mutable values from selectors.I will make this diff into a PR on the repo, but wanted to mark this as an issue beforehand to gauge reactions.
Edit: Just to note, I am using reselect v4 here as we have not migrated yet. I didn't see this as a breaking change in the "What's New in 5.0.0?" so it is likely still an issue.
The text was updated successfully, but these errors were encountered: