Recoil: Is there a recommended way to put atoms into an atomFamily

Created on 17 Oct 2020  路  6Comments  路  Source: facebookexperimental/Recoil

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.

question

Most helpful comment

  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

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));
  }
});

All 6 comments

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:

  1. put new items into it, keyed by a primitive id
  2. find a single item by key
  3. find multiple items from a key list
  4. update and delete a single item, delete can be done via a 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?

  1. I fetched a list of items from a remote endpoint
  2. For each item in this list, I need to create an atom inside atomFamily named itemsFamily
  3. Then I render an Item component for each item key, inside component it tries to get the item via useRecoilValue(itemsFamily(props.id)) hook
  4. Item 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 set function? Since useSetRecoilValue is 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));
  }
});
Was this page helpful?
0 / 5 - 0 ratings