Recoil: [Feature request] add a way to use reducers inside selectors: useRecoilSelectorWithReducer and stateful selectors

Created on 15 May 2020  路  5Comments  路  Source: facebookexperimental/Recoil

So I was playing around with recoil and how it can be used in real world/enterprise apps. I had so many AHAs to be honest. I highly congratulate you for the hard work that was done.

I just started digging in the source code, so I am not qualified for contributing to solve bugs and/or add new features.

  1. So, the base entity or the atom in enterprise apps would be form management, and I find that forms are segmented into two types: inline forms and composed forms.
  • Inline forms are the ones only with one level deep: direct properties inside the main object. For this type of forms, they are trivial to manage. The following example manages them pretty good:
const MY_FORM_ATOM = atom({
  key: 'myForm',
  default: { username: '', password: '', dateOfBirth: '', rememberMe: true },
});
const MY_FORM = selector({
  key: 'myFormManagement',
  get: ({ get }) => get(MY_FORM_ATOM),
  set: ({ get, set }, { target: { name, value, type, checked } }) => {
    set(MY_FORM_ATOM, { ...get(MY_FORM_ATOM), [name]: type === 'checkbox' ? checked : value });
  },
});

function MyFormComponent() {
  const [values, updater] = useRecoilState(MY_FORM);
  return (
    <div>
      <input name="username" value={values.username} onChange={updater} />
      {/* And so on for the other fields */}
    </div>
  );
}
  • Composed forms where the main object has multiple levels of nested objects, ie: User { username, fullName, age, addresses: [{city, country, zipCode, description}], todos: [{id, description, completed}]. The previous solution will be unfortunate since the updater will have to preserve the whole state while updating a small portion of it and the code will look very creepy. So, I came out with this: useRecoilSelectorWithReducer
function useRecoilSelectorWithReducer({ selectorKey, selectorAtom, reducer }) {
  return useRecoilState(
    selector({
      key: selectorKey,
      get: ({ get }) => get(selectorAtom),
      set: ({ get, set }, action) => {
        set(selectorAtom, reducer(get(selectorAtom), action));
      },
    }),
  );
}

This allows us to benefit from reducers and their power and do like these things:

function inlineUpdater(e) {
  const { name, value, type, checked } = e.target;
  updateValue({ type: 'change_value', payload: { name, value, type, checked } });
}
function changeAddressField(id, event) {
  const { name, value } = event.target;
  updateValue({ type: 'change_address', payload: {id, name, value } });
}
function addAddress() {
  updateValue({ type: 'add_address', payload: {id, description, country, city } });
}
function removeAddress(id) {
  updateValue({ type: 'remove_address', payload: id });
}

So my request here, Is there any way that you can adopt/design something similar out of the box ? I think the usage of a reducer to update a state will have a major power in managing complex states.
The current implementation I did, changes the value of the setter at every render, and I don't feel ok about it. (May be I have to think more about a way to do it)

  1. Is there any way to include stateful selectors? ie: selectors that accepts and an argument and returns a subscribed sub-state ? for example, If we take the todo list example from the docs, when updating a todo, all todos keep being re-rendered because they are subscribed to the global todo list, One solution is to create an atom per todo, but what If I want to send the whole list to my api ? I did not find any way to select atoms by regex or pattern. I came up with following hack, but again, there are memoization issue regarding the setter:
export const TODO_BY_ID = (id) =>
  selector({
    key: 'getTodoById',
    get: ({ get }) => get(TODOS_ATOM)[id],
    set: ({ get, set }, value) => {
      set(TODOS_ATOM, { ...get(TODOS_ATOM), [value.id]: { ...value} });
    },
  });

I understand that you may be overwhelmed by the huge amount of issues and requests, but for me, this is a sign of success. So I did my best to group my thoughts in this issue.

Thank you again for the great library

Most helpful comment

Part of what is nice about Recoil is that you can build more complex helpers on top of the simple atom and selector building blocks. You may be interested in the selectorFamily helper. This pattern wraps a selector in a function and basically allows you to pass parameters to the get/set functions.

For example:

const mySelectorFamily = selectorFamily({
  key: 'MySelectorFamily',
  get: params => ({get}) => { get stuff based on params },
  set: params => ({set, get}) => { set stuff based on params }<
});

Then use it like:

  const [value, setValue] = useRecoilState(mySelectorFamily({foo}));

One thing nice about this approach is that exposing it as a function which returns a selector means we can then compose and use it by other selectors, which you can't do with wrapping it with a hook. atomFamily and selectorFamily are some of the available helpers in the RecoilUtils.js module. I think we have a PR in the works to properly export that in the build.

But, as you can see, you're also free to make cool helpers or abstractions with hooks like you demonstrate here. Thanks for taking such a close look at Recoil!

All 5 comments

I solved the memoization issue I had with my useRecoilSelectorWithReducer.

I was passing a new created selector each time to useRecoilState. So the new version looks like this:

export function useRecoilSelectorWithReducer({ selectorKey, selectorAtom, reducer }) {
  const memoizedSelector = React.useMemo(
    () =>
      selector({
        key: selectorKey,
        get: ({ get }) => get(selectorAtom),
        set: ({ get, set }, action) => {
          set(selectorAtom, reducer(get(selectorAtom), action));
        },
      }),
    [selectorKey, selectorAtom, reducer],
  );
  return useRecoilState(memoizedSelector);
}

Part of what is nice about Recoil is that you can build more complex helpers on top of the simple atom and selector building blocks. You may be interested in the selectorFamily helper. This pattern wraps a selector in a function and basically allows you to pass parameters to the get/set functions.

For example:

const mySelectorFamily = selectorFamily({
  key: 'MySelectorFamily',
  get: params => ({get}) => { get stuff based on params },
  set: params => ({set, get}) => { set stuff based on params }<
});

Then use it like:

  const [value, setValue] = useRecoilState(mySelectorFamily({foo}));

One thing nice about this approach is that exposing it as a function which returns a selector means we can then compose and use it by other selectors, which you can't do with wrapping it with a hook. atomFamily and selectorFamily are some of the available helpers in the RecoilUtils.js module. I think we have a PR in the works to properly export that in the build.

But, as you can see, you're also free to make cool helpers or abstractions with hooks like you demonstrate here. Thanks for taking such a close look at Recoil!

Pretty clear for me now, I didn't knew about the atomFamily stuff.

Is there any plans to add a comparator to selectors ? ie: prevent re-renders if the comparator decides so. This will be very handy, for example if I want to select a substate, and not re-render until a value from it changes, but do not re-render if something in the whole atom changes.

For people looking for a reducer example without using hooks, here it is

export function reducerSelector({ selectorKey, selectorAtom, reducer }) {
  return selector({
    key: selectorKey,
    get: ({ get }) => get(selectorAtom),
    set: ({ get, set }, action) => {
      set(selectorAtom, reducer(get(selectorAtom), action));
    },
  });
}

usage:

const myReducerSelector = reducerSelector({
  selectorKey: 'myFormManager',
  reducer: myFormReducer,
  selectorAtom: MY_FORM_ATOM,
});

Yes, actually. We're looking at a few options for the comparator concept.

For your reducer example, also note that we have an updater form of the setter:

  set(selectorAtom, previousValue => reducer(previousValue, action));

Thanks for the hint.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ibnumusyaffa picture ibnumusyaffa  路  4Comments

Sawtaytoes picture Sawtaytoes  路  4Comments

aappddeevv picture aappddeevv  路  3Comments

adrianbw picture adrianbw  路  3Comments

polemius picture polemius  路  3Comments