I was recently discovering recoil to manage an entity store like redux with normalization, I think atomFamily is a best to manage entities identified by a unique key:
const todosState = atomFamily({key: 'entities/todo', default: null});
I then encountered an issue where I have already fetched a list of todos and want to put them into this atom family:
const todosState = atomFamily({key: 'entities/todo', default: null});
// Containes ids of todo for current list
const todoKeysState = atom({key: 'todo/keys', default: []});
const TodoItem = ({id}) => {
const todo = useRecoilValue(todosState(id));
return (
<li>
{todo.text}
</li>
);
};
const TodoList = () => {
const [todoKeys, setTodoKeys] = useRecoilState(todoKeysState);
useEffect(
() => {
(async () => {
const todos = await fetch('/api/todos');
setTodoKeys(todos.map(t => t.id));
for (const todo of todos) {
// How to put this into todosState?
}
})();
},
[]
);
return (
<ul>
{todoKeys.map(k => <TodoItem key={k} id={k} />)}
</ul>
)
};
Although I can fallback to use a single atom to store all todos and use selectorFamily to reach each todo item, its performance can't satisfy me.
Now I introduce a Map object to store initial values of each particular atom, use a default option to link atomFamily and this map:
import {atomFamily, RecoilState} from 'recoil';
interface EntityStore<E, K = string> {
name: string;
initial: Map<K, E>;
family: (key: K) => RecoilState<E>;
}
export function createEntityStore<E, K = string>(name: string): EntityStore<E, K> {
const initial = new Map<K, E>();
return {
name,
initial,
family: atomFamily<E, K>({
key: `entities/${name}`,
default: (key) => initial.get(key),
}),
};
}
I don't think this s a best practice, is there any recommended way to manage entities inside atomFamily so that we can:
deleted flag"Deleting" items from an atomFamily() container can be done using useResetRecoilState() hook. In your example I don't see that initial is ever initializing or passing in the default values. I guess it could be set later, but safer to guarantee it's set before the atomFamily() attempts to reference it for default values. What other issues are you having?
itemsFamilyItem component for each item key, inside component it tries to get the item via useRecoilValue(itemsFamily(props.id)) hookItem component can also fetch new value of its owned item and update corresponding atom state via useSetRecoilState(itemsFamily(props.id))For now I can't figure a good way to archive step 2, I use initial Map to store initial item values so that a non-existing atom can reference this from Map in step 3
Atoms in an atomFamily() are created on first-use. So, you can set the value of the atom when parsing the query if you're handling that imperatively, e.g.:
set(itemsFamily(props.id), valueFromQuery);
If you want to hook it up so the items automatically query for their default values and have a pending state while the query is pending, you could use a selectorFamily() as the default:
// query that returns an object of Item IDs to initial values
const initialItemValuesQuery = selector({
key: 'InitialItemValues',
get: ({get}) => ...fetch object of initial item values...
});
const itemsFamily = atomFamily({
key: 'Items',
default: selectorFamily({
key: 'Items/Default',
get: id => ({get}) => get(initialItemsQuery).id,
}),
});
set(itemsFamily(props.id), valueFromQuery);
How can we implement this set function? Since useSetRecoilValue is a hook that cannot be called in loop or async callback, imperative set seems impossible to me
set(itemsFamily(props.id), valueFromQuery);How can we implement this
setfunction? SinceuseSetRecoilValueis a hook that cannot be called in loop or async callback, imperative set seems impossible to me
You can use set's in a useRecoilCalback() in a loop or async.
const setListItems = useRecoilCallback(listItems => ({set}) => {
for (const item of listItems) {
set(itemsFamily(item.id, item));
}
});
Most helpful comment
You can use
set's in auseRecoilCalback()in a loop or async.