乐闻世界logo
搜索文章和话题

Recoil 如何获得原子族 atomfamily 的所有元素?

4 个月前提问
2 个月前修改
浏览次数70

5个答案

1
2
3
4
5

在 Recoil 中,atomFamily 是一个工具函数,它允许我们创建一组相关的 atoms,每一个 atom 都有一个独特的参数作为标识。然而,Recoil 原生并没有直接提供一个函数可以一次性获取 atomFamily 中所有的元素。但是,我们可以通过跟踪使用过的参数来间接获取所有的元素。

要追踪一个 atomFamily 中所有的元素,你可以创建一个 Recoil selector,这个 selector 会追踪每个被创建和使用过的 atom。每次使用 atomFamily 的时候,你可以将其参数添加到一个全局的集合中,并通过这个集合来知道哪些 atomFamily 的成员是被访问过的。

例如,以下是如何实现这个功能的一个简单例子:

javascript
import { atomFamily, selector, useRecoilValue } from 'recoil'; // 定义一个用于追踪访问过的 atomFamily 成员的集合 const atomFamilyParametersSet = new Set(); // 一个示例 atomFamily const myAtomFamily = atomFamily({ key: 'MyAtomFamily', default: param => { // 这里添加逻辑来确定你的默认值 return defaultValueBasedOnParam(param); }, effects_UNSTABLE: param => [ ({setSelf, onSet}) => { // 当atomFamily的成员被创建时,将其参数添加到集合中 atomFamilyParametersSet.add(param); onSet((newValue, oldValue, isReset) => { // 如果有必要,也可以在这里处理值的变化 }); }, ], }); // 定义一个 selector 来追踪和获取所有已访问的 atomFamily 成员 const allMyAtomFamilyMembers = selector({ key: 'AllMyAtomFamilyMembers', get: ({ get }) => { // 将所有追踪过的参数转换成对应的 atomFamily 成员的值 const allAtoms = Array.from(atomFamilyParametersSet).map(param => get(myAtomFamily(param)) ); return allAtoms; }, }); // 使用 React 组件时,可以这样获取所有 atomFamily 成员的状态 const MyComponent = () => { const allMembers = useRecoilValue(allMyAtomFamilyMembers); // 渲染所有 atomFamily 成员的信息... };

在这个例子中,我们创建了一个 atomFamily,并使用 effects_UNSTABLE 进行了一个效果的设置,在每次这个 atom 被创建时,我们会将其参数添加到 atomFamilyParametersSet 集合中。然后,我们定义了一个 selector 来获取和追踪所有已经访问过的 atomFamily 成员,我们利用这个 selector 来得到这些成员的状态。最后,在组件中使用 useRecoilValue 来获取所有成员的状态并进行渲染。请注意,这个方法只能跟踪到被实际使用过(即被 getset)的 atomFamily 成员。未被使用的成员不会被添加到集合中。

2024年6月29日 12:07 回复

Instead of using useRecoilCallback you can abstract it with selectorFamily.

shell
// 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:

shell
// 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.

shell
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:

shell
// 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)]); } });
2024年6月29日 12:07 回复

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.

shell
// 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

shell
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:

shell
createMeal("bananas", 5);

And get all ids via:

shell
const ids = useRecoilValue(mealIds);
2024年6月29日 12:07 回复

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.

shell
// 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:

shell
// 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()

shell
// 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.

shell
// 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.

2024年6月29日 12:07 回复

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)

shell
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:

shell
... 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):

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

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

shell
... const resetFormState = useResetRecoilState(formStateSelector([])); ... const handleDiscard = React.useCallback(() => { ... resetFormState(); ... }, [..., resetFormState]);
2024年6月29日 12:07 回复

你的答案