React JS & Recoil set/get on select/selectFamily for specific attribute

Viewed 50

I use Recoil state management in ReactJS to preserve a keyboard letters data, for example

lettersAtom = atom(
  key: 'Letters'
  default: {
     allowed : ['A','C','D']
     pressedCounter : {'A':2, 'D':5}
  }
)

lettersPressedSelect = selector({
        key: 'LettersPressed',
        get: ({ get }) => get(lettersAtom).pressedCounter, //Not work, returns undefined
        set: () => ({ set }, pressedLetter) => {
            let newState = {...lettersAtom};
            newState.pressedCounter[pressedLetter]++;
            set(lettersAtom, newState);
        }
}),

In functional component i use

const [letters,setLetters] = useRecoilState(lettersAtom);
const [pressedCounter, setPressedCounter] = useRecoilState(lettersPressedSelect);

each time the a keyboard letter pressed the pressedCounter I want to increased for corresponded letter like that

setPressedCounter('A');
setPressedCounter('C'); ///etc...

How to achieve that ? Does recoil have a way to get/set a specific part/sub of json attribute ? (without make another atom? - I want to keep "Single source of truth") Or do you have a suggetion better best practice to do that ?

3 Answers

There are some bugs in your code: no const, braces in atom call and no get inside the set. You also need spread the pressedCounter.

Overwise your solution works fine.

In Recoil you update the whole atom. So in this particular case you probably don't need the selector. Here is a working example with both approaches:

https://codesandbox.io/s/modest-wind-kosp7o?file=/src/App.js

It a best-practice to keep atom values rather simple.

You can update the state based on the existing state in a selector in a couple ways. You could use the get() callback from the setter or you could use the updater form of the setter where you pass a function as the new value which receives the current value as a parameter.

However, it's a good practice to have symmetry for the getter and setters of a selector. For example, here's a selector family which gets and sets the value of a counter:

const lettersPressedState = selectorFamily({
    key: 'LettersPressed',
    get: letter => ({ get }) => get(lettersAtom).pressedCounter[letter],
    set: letter => ({ set }, newPressedValue) => {
        set(lettersAtom, existingLetters => ({
            ...existingLetters,
            pressedCounter: {
                ...existingLetters.pressedCounter,
                [letter]: newPressedValue,
            },
        });
    },
});

But note that the above will set the new value with a new counter value where you originally wanted the setter to increment the value. That's not really setting a new value and is more like an action. For that you don't really need a selector abstraction at all and can just use an updater when setting the atom:

  const [letters, setLetters] = useRecoilState(lettersAtom);
  const incrementCounter = pressedLetter =>
      setLetters(existingLetters => ({
          ...existingLetters,
          pressedCounter: {
              ...existingLetters.pressedCounter,
              [pressedLetter]: (existingLetters.pressedCounter[pressedLetter] ?? 0) + 1,
          },
      });

Note that this uses the updater form of the selector to ensure it is incrementing based on the current value and not a potentially stale value as of the rendering.

Or, you can potentially simplify things more and use simpler values in the atoms by using an atom family for the pressed counter:

const pressedState = atomFamily({
    key: 'LettersPressed',
    default: 0,
});

And you can update in your component like the following:

  const [counter, setCounter] = useRecoilState(pressedState(letter));
  const incrementCounter = setCounter(x => x + 1);

Or create an general incrementor callback:

  const incrementCounter = useRecoilCallback(({ set }) => pressedLetter => {
      set(pressedState(pressedLetter)), x => x + 1 );
  });

So the sort answer help by user4980215 is:

set: () => ({ get, set }, pressedLetter) => {
            let newState = {...get(lettersAtom)};
            newState.pressedCounter[pressedLetter]++;
            set(lettersAtom, newState);
        }
Related