Recoiljs: fresh state management

Yes, it will never end, and this is so new, that their site has empty pages. But from a fast check, Recoil looks like light white state management that provides the benefits of a shared state without the ungainly of redux. Interestingly, it came out exactly at the moment I was looking for something like that. So I have taken it for a ride.

Recoil API’s and explanation will come after the hangman #notFancyAppChallenge. Just note that as the Recoil website is still unfilled, and has plenty of empty pages, this #notFancyAppChallenge will also not explain every API and util.

Recoil hangman

Recoil Data objects. I marked the Recoil keywords and usage.

const gameBoard = atom({
  key: 'gameBoard',
  default: {
    word: getRandomWord(),
    selectedLetters: ['d','f','g']
  }
})
const availableLettersList = selector({
  key: 'AvailableLettersList',
  get: ({get}) => {
    const board = get(gameBoard);
    const selected = board.selectedLetters;
    const selectedWord = selected.join();
    const letters = 'abcdefghijklmnopqrstuvwxyz'.split('');
    return letters.filter(l => selectedWord.indexOf(l) == -1);
  }
})

Some helper JS functions

const turns = 7;
const getRandomWord = () => {
  const words = ['Recoil demo','not so fancy app challenge', 'will it work','will i use it'];
  return words[Math.round(Math.random()*words.length)];
}

The React components:

const App = () => {
  return (
    <RecoilRoot>
      <Word />
      <SelectedLetters />
      <AvailableLetters />
      <Reset />
    </RecoilRoot>
  )
}

const Word = () => {
  const board = useRecoilValue(gameBoard);
  const selected = board.selectedLetters.join('');
  const theWord = board.word.split('').map(l =>{
    return l == ' ' || selected.indexOf(l) > -1? l:'_';
  })
  return <>The word: {theWord}</>
}

const SelectedLetters = () => {
  const board = useRecoilValue(gameBoard);
  return (
    <p>
      Selected: {board.selectedLetters.join(', ')},
      {new Array(turns -       board.selectedLetters.length).fill('_').join(', ')}
    </p>
  )
}

const AvailableLetters = () => {
  const letters = useRecoilValue(availableLettersList);
  const [board,setBoard] = useRecoilState(gameBoard);
  const clickLetter = (l)=> {
    if(turns > board.selectedLetters.length){
      setBoard({
        word: board.word,
        selectedLetters: [l,...board.selectedLetters]
      });
    }
  }
  return (
    <p>
      Select: {letters.map(l => <b key={l} onClick={() => {clickLetter(l)}}>{l}</b>)},
    </p>
  )
}

const Reset = () => {
  const [game, resetGame] = useRecoilState(gameBoard);
  const reset = () => {
    resetGame({
      word: getRandomWord(),
      selectedLetters: []
    })
  }
  return <button onClick={reset}>reset</button>
}

Installation:

npm install recoil
yarn add recoil

RecoilRoot

To use Recoil, you need to wrap the components with a Recoil element. Somewhere in the docs they mention that you can define the constructor for the Aroms used according to the root.

function App = () => {
  return (
    <RecoilRoot>
      <SomeStuffInside />
    </RecoilRoot>
  );
}

Atom

We need a name for these pieces of state we’re gonna call them Adams. So, an atom is a changeable subscribable unit of state.

David McCabe: Recoil at React Europe 2020

Atom is the basic building block of Recoil, and presents the data that changes and need to be observed along with the app.

While defining atom you need to provide a unique key, and can define the default value.

const userName = atom({
  key: 'userName', 
  default: 'Jone Smith', 
});

You can use objects and lists. But the advantage of using separate atoms for each element over a single atom with a map, is that small atoms maintain their own individual subscriptions. So, updating the value for one element will only cause React components that have subscribed to just that atom to update.

const hangmanBoard = atom({
  key: 'hangmanBoard', 
  default: {
    word: '',
    usedLetters: []
  }, 
});

Selector: the Recoil computed value

Selector is basically a pure function plus information about what state it depends on so that when that state changes we can recompute the function and recompute and rerender any components that depend on it

David McCabe: Recoil at React Europe 2020
const userHiddenName = selector({
  key: 'userHiddenName',
  get: ({get}) => {
    const userName = get(userName);
    return Array(userName.length).fill('_').join('')
  },
  //set: ({set}, newValue) => { set(userName,'someValue');}
})

Async selectors

const fetchSomeData = selector({
  key: 'fetchSomeData',
  get: async ({get}) => {
    const response = await fetch(...
    return response;
  }
});

isRecoilValue

Used to chack if a value is a Recoil object

const recoilAtom = atom(...
const recoilSelector = selector(...

isRecoilValue(recoilAtom); // true
isRecoilValue(recoilSelector); // true

Hooks:

useRecoilValue

Used to observe an atom or selector:

const SomeStuffInside = () =>{
  const userName = useRecoilValue(userName);
  return (
    <b onClick={hideName}>You name is {setName}</b>
  )
}

useRecoilState

As useState in React, a getter and setter for the atom:

const SomeStuffInside = () =>{
  const [userName,setName] = useRecoilState(userName);
  const hideName = () => {
    setName('Im a ghost');
  }
  return (
    <b onClick={hideName}>You name is {userName}</b>
  )
}

And as in the other observables, to update a list or an object, you need to recreate it:

const Letter = (props) => {
  const letter = props.letter;
  const [hangmanBoard,resetBoard] = useRecoilState(hangmanBoard);
  const addLetter = () => {
    resetBoard({
      word: hangmanBoard.word,
      usedLetters: [letter,...hangmanBoard.usedLetters]
    });
  }
  return (
    <span onClick={addLetter}>{letter}</span>
  )
}

useSetRecoilState

“Returns a setter function for updating the value of writeable Recoil state.”

useResetRecoilState

Returns a function that will reset the value of the given state to its default value.

useRecoilValueLoadable

“Used for reading the value of asynchronous selectors. This hook will implicitly subscribe the component to the given state”

useRecoilStateLoadable

useRecoilCallback

useTransactionalObservation

While the hooks above are mentioned on the Repoid site, sometimes with an empty page. This one I had seen it mentioned only in the video, it didn’t appear on the Recoil site :-/. Same as the withStateChange

Recoil utils

elementPositionStateFamily

Returns a function that returns a Recoil atom. Same as Atom but the default value can also be parameterized. 

const newGameBoard = atomFamily({
  key: someUniquKey,
  default: param => someFunctionToSetTheDefaultValue(param)
});

function newWord({wordText}) {
  const gameBoard = newGameBoard(wordText,wordText);
  return ...

selectorFamily

Similar to the selector, but allows you to pass parameters to the get and set callbacks of a selector.

constSelector

errorSelector

noWait

waitForAll

waitForAny

waitForNone

Conclusion

As written above, It came out exactly while I needed something like this. Although the FED community is very tired of the new libraries and frameworks that popup every day, Recoil seems smooth and simple, still is very strong and powerful. Let give it a day or two to settle down and fill the official website. Then it really might just be the missing piece we needed.