How to get all elements from an atomFamily in recoil?

11.5k views Asked by At

Im playing around with recoil for the first time and cant figure out how I can read all elements from an atomFamily. Let's say I have an app where a user can add meals:

export const meals = atomFamily({
  key: "meals",
  default: {}
});

And I can initialize a meal as follows:

const [meal, setMeal] = useRecoilState(meals("bananas"));
const bananas = setMeal({name: "bananas", price: 5});

How can I read all items which have been added to this atomFamily?

4

There are 4 answers

7
Johannes Klauß On BEST ANSWER

You have to track all ids of the atomFamily to get all members of the family. Keep in mind that this is not really a list, more like a map.

Something like this should get you going.

// atomFamily
const meals = atomFamily({
  key: "meals",
  default: {}
});

const mealIds = atom({
  key: "mealsIds",
  default: []
});

When creating a new objects inside the family you also have to update the mealIds atom.

I usually use a useRecoilCallback hook to sync this together

  const createMeal = useRecoilCallback(({ set }) => (mealId, price) => {
    set(mealIds, currVal => [...currVal, mealId]);
    set(meals(mealId), {name: mealId, price});
  }, []);

This way you can create a meal by calling:

createMeal("bananas", 5);

And get all ids via:

const ids = useRecoilValue(mealIds);
4
Bnaya On

Instead of using useRecoilCallback you can abstract it with selectorFamily.

// atomFamily
const mealsAtom = atomFamily({
  key: "meals",
  default: {}
});

const mealIds = atom({
  key: "mealsIds",
  default: []
});

// abstraction
const meals = selectorFamily({
  key: "meals-access",
  get:  (id) => ({ get }) => {
      const atom = get(mealsAtom(id));
      return atom;
  },
  set: (id) => ({set}, meal) => {
      set(mealsAtom(id), meal);
      set(mealIds (id), prev => [...prev, meal.id)]);
  }
});

Further more, in case you would like to support reset you can use the following code:

// atomFamily
const mealsAtom = atomFamily({
  key: "meals",
  default: {}
});

const mealIds = atom({
  key: "mealsIds",
  default: []
});

// abstraction
const meals = selectorFamily({
  key: "meals-access",
  get:  (id) => ({ get }) => {
      const atom = get(mealsAtom(id));
      return atom;
  },
  set: (id) => ({set, reset}, meal) => {
      if(meal instanceof DefaultValue) {
        // DefaultValue means reset context
        reset(mealsAtom(id));
        reset(mealIds (id));
        return;
      }
      set(mealsAtom(id), meal);
      set(mealIds (id), prev => [...prev, meal.id)]);
  }
});

If you're using Typescript you can make it more elegant by using the following guard.

import { DefaultValue } from 'recoil';

export const guardRecoilDefaultValue = (
  candidate: unknown
): candidate is DefaultValue => {
  if (candidate instanceof DefaultValue) return true;
  return false;
};

Using this guard with Typescript will look something like:

// atomFamily
const mealsAtom = atomFamily<IMeal, number>({
  key: "meals",
  default: {}
});

const mealIds = atom<number[]>({
  key: "mealsIds",
  default: []
});

// abstraction
const meals = selectorFamily<IMeal, number>({
  key: "meals-access",
  get:  (id) => ({ get }) => {
      const atom = get(mealsAtom(id));
      return atom;
  },
  set: (id) => ({set, reset}, meal) => {
      if (guardRecoilDefaultValue(meal)) {
        // DefaultValue means reset context
        reset(mealsAtom(id));
        reset(mealIds (id));
        return;
      }
      // from this line you got IMeal (not IMeal | DefaultValue)
      set(mealsAtom(id), meal);
      set(mealIds (id), prev => [...prev, meal.id)]);
  }
});
1
Jamie Kingston On

Here is how I have it working on my current project:

(For context this is a dynamic form created from an array of field option objects. The form values are submitted via a graphql mutation so we only want the minimal set of changes made. The form is therefore built up as the user edits fields)

import { atom, atomFamily, DefaultValue, selectorFamily } from 'recoil';

type PossibleFormValue = string | null | undefined;

export const fieldStateAtom = atomFamily<PossibleFormValue, string>({
  key: 'fieldState',
  default: undefined,
});

export const fieldIdsAtom = atom<string[]>({
  key: 'fieldIds',
  default: [],
});

export const fieldStateSelector = selectorFamily<PossibleFormValue, string>({
  key: 'fieldStateSelector',
  get: (fieldId) => ({ get }) => get(fieldStateAtom(fieldId)),
  set: (fieldId) => ({ set, get }, fieldValue) => {
    set(fieldStateAtom(fieldId), fieldValue);
    const fieldIds = get(fieldIdsAtom);
    if (!fieldIds.includes(fieldId)) {
      set(fieldIdsAtom, (prev) => [...prev, fieldId]);
    }
  },
});

export const formStateSelector = selectorFamily<
  Record<string, PossibleFormValue>,
  string[]
>({
  key: 'formStateSelector',
  get: (fieldIds) => ({ get }) => {
    return fieldIds.reduce<Record<string, PossibleFormValue>>(
      (result, fieldId) => {
        const fieldValue = get(fieldStateAtom(fieldId));
        return {
          ...result,
          [fieldId]: fieldValue,
        };
      },
      {},
    );
  },
  set: (fieldIds) => ({ get, set, reset }, newValue) => {
    if (newValue instanceof DefaultValue) {
      reset(fieldIdsAtom);
      const fieldIds = get(fieldIdsAtom);
      fieldIds.forEach((fieldId) => reset(fieldStateAtom(fieldId)));
    } else {
      set(fieldIdsAtom, Object.keys(newValue));
      fieldIds.forEach((fieldId) => {
        set(fieldStateAtom(fieldId), newValue[fieldId]);
      });
    }
  },
});

The atoms are selectors are used in 3 places in the app:

In the field component:

...
const localValue = useRecoilValue(fieldStateAtom(fieldId));
const setFieldValue = useSetRecoilState(fieldStateSelector(fieldId));
...

In the save-handling component (although this could be simpler in a form with an explicit submit button):

...
const fieldIds = useRecoilValue(fieldIdsAtom);
const formState = useRecoilValue(formStateSelector(fieldIds));
...

And in another component that handles form actions, including form reset:

...
const resetFormState = useResetRecoilState(formStateSelector([]));
...
const handleDiscard = React.useCallback(() => {
  ...
  resetFormState();
  ...
}, [..., resetFormState]);
2
TomasLangebaek On

You can use an atom to track the ids of each atom in the atomFamily. Then use a selectorFamily or a custom function to update the atom with the list of ids when a new atom is added or deleted from the atomFamily. Then, the atom with the list of ids can be used to extract each of the atoms by their id from the selectorFamily.

// File for managing state


//Atom Family
export const mealsAtom = atomFamily({
  key: "meals",
  default: {},
});
//Atom ids list
export const mealsIds = atom({
  key: "mealsIds",
  default: [],
});

This is how the selectorFamily looks like:

// File for managing state

export const mealsSelector = selectorFamily({
  key: "mealsSelector",
  get: (mealId) => ({get}) => {
    return get(meals(mealId));
  },
  set: (mealId) => ({set, reset}, newMeal) => {
    // if 'newMeal' is an instance of Default value, 
    // the 'set' method will delete the atom from the atomFamily.
    if (newMeal instanceof DefaultValue) {
      // reset method deletes the atom from atomFamily. Then update ids list.
      reset(mealsAtom(mealId));
      set(mealsIds, (prevValue) => prevValue.filter((id) => id !== mealId));
    } else {
      // creates the atom and update the ids list
      set(mealsAtom(mealId), newMeal);
      set(mealsIds, (prev) => [...prev, mealId]);
    }
  },
});

Now, how do you use all this?

  • Create a meal:

In this case i'm using current timestamp as the atom id with Math.random()

// Component to consume state

import {mealsSelector} from "your/path";
import {useSetRecoilState} from "recoil";
const setMeal = useSetRecoilState(mealsSelector(Math.random()));

setMeal({
    name: "banana",
    price: 5,
});

  • Delete a meal:
// Component to consume state

import {mealsSelector} from "your/path";
import {DefaultValue, useSetRecoilState} from "recoil";

const setMeal = useSetRecoilState(mealsSelector(mealId));
setMeal(new DefaultValue());
  • Get all atoms from atomFamily:

Loop the list of ids and render Meals components that receive the id as props and use it to get the state for each atom.

// Component to consume state, parent of Meals component

import {mealsIds} from "your/path";
import {useRecoilValue} from "recoil";

const mealIdsList = useRecoilValue(mealsIds);

    //Inside the return function:
    return(
      {mealIdsList.slice()
          .map((mealId) => (
            <MealComponent
              key={mealId}
              id={mealId}
            />
          ))}
    );
// Meal component to consume state

import {mealsSelector} from "your/path";
import {useRecoilValue} from "recoil";

const meal = useRecoilValue(mealsSelector(props.id));

Then, you have a list of components for Meals, each with their own state from the atomFamily.