I'm doing something like this:
export const userListState = selector({
key: 'userStateList',
get: async () => {
const resp = await fetch('/api/users')
const users = await resp.json()
users.forEach((user) => userState(user)
return users.map(({ key }) => key))
},
})
export const userState = atomFamily({
key: 'userState',
default: user => user
})
Ideally, within my app, I'd like to iterate through userListState, and and map an array of <User userKey={key} /> components
const User = ({ userKey }) => {
const user = useRecoilValue(userState(userKey))
...
}
But given the way I instantiate each userState atom, I can't pass the key in, I need to pass the entire object. So in a perfect world, the API for the default function would look like this:
const userState = atomFamily({
'userState',
default: (key, user) => user
})
I can get deeper into the need for this design, with each user having its own atom, as opposed to an atom consisting of a single array containing all users, but hopefully it's not needed right now.
Does the API support something like this, or is there some kind of pattern I can apply? Loving recoil, but I'm hitting a wall with this!!
I'm having a hard time understanding what it is you're trying to accomplish. If you have a selector returning an array of these objects, you could create a selectorFamily that given a key finds the one you want from that selector (at which point you want a map rather than an array). In addition if you don't want the entire object, you could create a selectorFamily that given an object returns the field you want.
But given the way I instantiate each userState atom, I can't pass the key in, I need to pass the entire object.
Why is this the case?
This example of yours:
export const userState = atomFamily({
key: 'userState',
default: user => user
})
is problematic because it is using the entire object as the key. So, if you changed the state of a user based on the default key then they don't match and you would need to access the new state with the old default. Or, if you access with the new state then you get a different atom.
Will @Shmew 's suggestion of a selectorFamily() referenced by id work to select the user from the query selector?
@Shmew You're right, more context is needed. I don't think what you proposed would work for my case, because in addition to reading the in a user object, I also want to set the object.
The app consists of two types of main components, <User/> and <UserDetails/>.
The User components are in a list, and show some meta level about the user (ie name), and the UserDetails component consists of the selected User (which is set by clicking on one of the User components). The UserDetails lists more data about the chosen user, and allows you to edit the user. This edit will update the info rendered in the corresponding User component as well.
So given this design, there will have to be a single userState atom for each user. I want to be able to update the UserDetails without rerendering ALL of the User components.
Someone wrote a medium article with a nicely contrived example of precise updates, and the code for it can be found here. It's similar to mine, only theirs is made simpler because they do a fetch per item, and then make an atom out of each response. Mine is made more difficult in that I fetch all items at once
I've been messing with lots of combinations of selectors and atoms this, maybe the API doesn't support this need at this time.
Okay I think I have a better understanding now.
I believe the reason this is difficult is because you're trying to modify what should be immutable. The result of your query shouldn't ever change, as if anything else wants this result, the integrity of the data may be compromised.
So I believe a solution is to have your normal selector that fetches all of your data.
Then to have an atomFamily that given a UserInfo creates an atom to hold modified user info.
Then wrap that atomFamily in a read-writeselectorFamily, that given an id will grab the UserInfo from the query results and send that to the atomFamily to fetch the modified UserInfo atom. The set of this would do the same thing, and then set the new value.
This should prevent any re-renders when you modify the atoms, as your original query selector will not have changed.
@drarmstr can tell us if I'm giving bad advice or not.
This seemed to work! So even though we set the userInfoAtom with an updates user object, the original key to access it remains to be the original object we passed in as an argument from the fetch?
const userInfoState = atomFamily({
key: 'userInfoState',
default: u => u
})
export const userState = selectorFamily({
key: 'userState',
get: (id) => ({ get }) => {
const user = get(userListState).get(id)
console.log({user})
return get(userInfoState(user))
},
set: (id) => ({ get, set }, { field, value }) => {
const user = get(userListState).get(id)
set(userInfoState(user), { ...user, [field]: value })
}
})
export const userListState = selector({
key: 'userStateList',
get: async () => {
const resp = await fetch('/api/users')
const users = await resp.json()
const userEntries = users.map((user, i) => [i, user])
return new Map(userEntries);
},
})
Correct, the "key" for userInfoState is the UserInfo that is returned by the query.
One potential problem with this is it can cause a memory leak if the atoms are persisted until #56 lands, or if your query is a selectorFamily (and called frequently) rather than a selector.
I should also mention that once #314 lands doing this kind of thing will be much less complex, as you can simply create an atom that has that selector as the default value and wrap it with a selectorFamily and modify it as you wish.
That sounds great - what would that look like codewise?
I'm not a JS developer, but something like:
export const userListState = selector({
key: 'userStateList',
get: async () => {
const resp = await fetch('/api/users')
const users = await resp.json()
const userEntries = users.map((user, i) => [i, user])
return new Map(userEntries);
},
})
export const getUserListState = selectorFamily({
key: 'getUserListState',
get: id => ({ get }) => {
return get(userListState).get(id)
}
})
export const userState = atomFamily({
key: 'userState',
default: id => getUserListState(id)
})