tl;dr: if an atom is an object and a selector returns a single property from that object, will downstream components re-render whenever ANY part of the atom changes, or only when that selector value changes? i.e. are selector output values cached or do re-renders occur whenever any of their input dependencies change? (I suspect the latter?).
If recoil doesn't cache selector output, then I suspect my best approach is useRecoilCallback to avoid subscribing, then have something like useStateSlice() which takes a slice fn() which triggers component updates (useState/setState) when the slice value differs?
seem right?
background:
This arises from a common(?) case:
The "atom" in my case is a (firebase) document which is basically a property bag of fields. We get async updates from firebase when that data is changed (possibly external to our app - e.g. by another user, etc.).
So if my atom is the document, I (think?) I'm hosed - any update to any field of the document will cause all downstream selectors to update and therefore all components depending on ANY part of the document to re-render.
What I'd like is to think of selectors as a "slicing" the data, so I could have a lastName selector which picks the lastName field out of the Person document. Components depending on lastName would update only when that field is different, ignoring changes to other parts of the person.
I just spent some time trying to see if there was some way to work around this.
I think the best solution is to find a way to make the properties the atoms rather than the document. Such that when you need to find one part, it's really only getting that one part.
If that isn't possible, I suspect the next best solution is to create an empty component that simply observes the document atom. Which you would then run checks on re-render to see if any of the atoms need to be modified. This means that not only do you need to have atoms defined for the parts of the document you want to subscribe to, but the component needs to be aware of them, and have logic to determine if the value has changed.
Yes, it's on our roadmap to have selectors suppress downstream updates and subscriptions if their computed value has not changed. I already have a diff for suppressing updates if setting an atom to the same value.
@drarmstr that would be very, very fantastic :-). I guess it opens up the can of "equality" worms in that selector functions would need to be careful to return the same (memoized?) instances as I assume it would be a reference equality comparison. But that's a huge advance over the current situation. Thanks for listening ;-)
Yeah, we would likely default to reference equality but allow for custom comparators or something. That would help avoid the need for crazy memoization hijinks. We haven't started thinking about the API yet, though; so not committing to anything at this point.
@Shmew thanks, Cody. Sounds good. Since I don't get any more granularity from firebase (the event fires for the whole doc/record, not per field), I'll make a "sync" component which subscribes to the firebase doc and then updates one atom (an atomFamily for that document) for each field in the doc if they value changes. thx for the suggestions.
@ehahn9 no problem! Can you leave the issue open so we can track the progress of this feature request? Once it's resolved you can implement it how you did originally.
Re-opening so we can track.
Glad this is on the roadmap and excited about this idea. When this lands, it will remove a lot of the complexity with object writes. Currently, Working on a an app with a normalized tree view with a bunch of properties in each leaf. Most updates aren鈥檛 bad, but there are some where multiple new leanes need to be added. This is super annoying because I have to set(itemPropAtomFamily({ id }), ...) for each item property in a useRecoilCallback. The ability to set(item({ id}), ... ) and then have selectors intelligently update would be ideal. Even better would be to retain the ability to use a selectorFamily setter and have it understand the relationship to the atomFamily.
In my opinion, when this lands, Recoil will be the absolute go-to solution for complex state management. It will also simplify the performance story and allow folks to fall into a pit of success.
In my opinion, when this lands, Recoil will be the absolute go-to solution for complex state management
It would be nice to see a Recoil vs MobX comparison because MobX is probably the most similar to Recoil.
I understand that this is still in development, but I just want to re-iterate what has been said above by saying that this package is useless from an optimization perspective until the following example App only re-renders the button that is clicked.
New to react, but I do see a lot of potential in recoil.
While optimizing selectors will be nice, it is not required to optimize and avoid re-renders. Current best-practice is to use individual atoms or atom families to represent state for managing subscriptions and re-renders (#480)
Handling external data where it returns a large object (which you can't control) is really not a nice story at the moment. Even just making the constSelector have this type of functionality would go a really long way, as then we can map the complex atom into multiple smaller selectors as needed without worrying about re-renders if a different part changes.
While optimizing selectors will be nice, it is not required to optimize and avoid re-renders. Current best-practice is to use individual atoms or atom families to represent state for managing subscriptions and re-renders (#480)
Just a note on this best practice, for my use case having an atom for every individual piece of state increases the complexity of my components by a huge amount.
What I am trying to achieve is form state that I can manage both on the field level and at the parent component level with some helpers (think setFieldValue(name, value), setFieldError(name, error) etc.) while the fields may not even be rendered (hidden behind a tab etc). This means that what I have to do is create an atom per field, store the keys for this atom somewhere, then wherever it's used I have to get the key somehow. I was just using a util to pluck the atom out depending on the name of the field, but all of this means that I have a bunch of extra utils + data structures to manage all of this.
What I really want (and how I currently have it implemented) is just an atom at the top level that looks something like this
const formAtom = atomFamily({
key: 'form',
// Would really like to decouple initialValues
// from the form atom key also
default: (id, initialValues) => ({
initialValues,
values: initialValues,
errors: {},
touched: {},
...etc
})
})
Where whenever my field renders, all I need is the name of the field and I can do something like this
const fieldValueSelector = selectorFamily({
key: 'fieldValue',
get: ({ name, formId }) => ({ get }) => {
const { values } = get(formAtom(formId));
return _.get(values, name) ?? 'default value'
},
set: ({ name, formId }) => ({ set }, newValue) => {
set(formAtom(formId), (prevForm) => ({
...prevForm,
values: _.set(_.cloneDeep(prevForm.values), name, newValue)
}));
}
})
This allows me to not even care about any kind of data structure to keep track of all the individual atoms for the field and literally just have the <Field /> component consume the entire form state and decide what to connect to depending on the name given to me (whether it's from the developer during setFieldValue or if it's a user changing a field).
I also can't just use the name as my field atom key because fields might change name during the lifetime of the component, if we're re-arranging fields within a list of fields of the same type for example.
Basically having a selector that doesn't re-render my component unless the returned value changes would save me a hundred lines of code and a lot of maintenance burden.
Yup, I agree that we really want to do this.
Does anyone have workaround for this in the meantime? (maybe you've come across one @drarmstr, or have an idea of how to implement one?)
I came up with a similar approach to you @aaronnuu, but I can't actually get that fully performant either. I've put together a barebones repro here: https://codesandbox.io/s/userecoilstateat-3lers
I've also tried using useRecoilCallback(), but I can't seem to get anything working dependably that doesn't have timing issues / race conditions (maybe because of how the async set()'s are queued?).
I would also like to emphasise on this matter. As mentioned and discussed in #522, the lack of optimisation on this topic seems be the only currently known "big issue" for us when it comes to Recoil, and would be amazing if it got implemented. Yes it is possible to avoid re-renders at the moment, but the solutions I've seen in multiple issues seems hackish, and some of them lead to boilerplate which I otherwise enjoy avoiding when using Recoil.
Just found this issue when trying to do something like the following:
export const selectedFooIdState = atom<string | undefined>({
key: "selectedFooIdState",
default: undefined,
});
export const isFooSelected = selectorFamily<boolean, string>({
key: "isFooSelected",
get: (fooId) => ({ get }) => {
return get(selectedFooIdState) === fooId;
},
});
With a list of <Foo> components that uses isFooSelected to determine if they are currently selected.
I was a bit surprised to find that all my <Foo>s re-rendered every time the selection state changed, as opposed to only the relevant selection state as determined by isFooSelected.
If I understand the above conversation the current recommendation for working around this limitation is to do one of the following:
useRecoilCallback to manually set component state if and only if the value has actually changedatomFamily to track each individual foo's selection state separatelyis this right?
For folks looking to do this workaround:
- use useRecoilCallback to manually set component state if and only if the value has actually changed
https://github.com/facebookexperimental/Recoil/issues/522#issuecomment-674567123 is a pretty good place to start
Partial fix in #749 - This will suppress re-rendering subscribing components when selectors have the same value (based on reference equality), though it will still re-evaluate downstream selectors (though uses caching to suppress actually calling the evaluation function). It may also help performance with a bit of pre-fetching async requests before rendering starts.
Just found out this same issue while nesting selectors.
Quick update: Using Atom Families triggers rerender too.
Current status is that the #749 optimization is still behind a feature flag. During stress testing we identified a situation where a component would not come out of suspense. We're not sure yet if it's a bug or user error.
Most helpful comment
Yes, it's on our roadmap to have selectors suppress downstream updates and subscriptions if their computed value has not changed. I already have a diff for suppressing updates if setting an atom to the same value.