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