RecoilJS is meant to rock your React world

ยท 9 min read
RecoilJS is meant to rock your React world

RecoilJS is a state management library for React which was made publicly available recently by Facebook . The truth is that they have been using it for some time internally, so they finally decided to open-source it ๐Ÿ˜

At the moment of this writing RecoilJS is considered an experimental set of utilities and its API keeps on improving rapidly. Personally speaking I have already tried to take advantage of its ways in a rather big application of mine, with complex state management and many dynamic connections. I have to admit that I got amazed with the efficiency, the simplicity and the flexibility it offers.

In my humble opinion RecoilJS makes React hell stronger. Let's see why.

# Why RecoilJS then?

No matter how much we love React we have to admit that there are some tricky parts or even pain points when it comes about state management with the Hooks API that is offered out of the box. To make it even more clear, we are talking mostly about React.useContext here and not about third party libraries like Redux.

One of the issues we face when it comes about state management, is that the children components should constantly inform the ancestors. This might seem quite simple for small applications but for more complex ones, things start getting ugly especially when we are talking about long components trees which we force to get re-rendered with every change triggered by a nested child.

Another issue is that context can store mostly single values and not complex variations needed by different consumers.

All these force the 2 sides of the components tree to be tightly coupled. As declared in RecoilJS docs:

Both of these make it difficult to code-split the top of the tree (where the state has to live) from the leaves of the tree (where the state is used).

So this is what RecoilJS is trying to accomplish. It tries to make our life easier by providing a Reactish API for even more flexible state management across complex applications.

In order to accomplish this, it defines a graph attached to our React components tree so that state changes flow from atoms which are the roots of this graph to our components through selectors which are pure functions.

Here is a small representation that might help ypu visualize what is actually going on with atoms:

RecoilJS is meant to rock your React world

# Show me an atom

Sure!! This is a dead-simple atom named itemsState:

import { atom } from 'recoil';

const itemsState = atom({
  key: 'itemsState',
  default: [{
    description: 'Don\'t be lazy, write the post of the week ๐Ÿ˜ฌ',
    done: false,
  }],
});

As we said atoms are units of state. Each atom has a unique identifier, and a default value. For our examples we can use a simple todo list.

RecoilJS offers some hooks we can use to get access to atoms state . We can use a hook named useRecoilState to access the list and apply changes to it or just the hook useRecoilValue to pull the list only.

Let's see a real life example then:

import React from 'react';
import { atom, useRecoilValue } from 'recoil';

const itemsState = atom({
  key: 'itemsState',
  default: [{
    description: 'Don\'t be lazy, write the post of the week ๐Ÿ˜ฌ',
    done: false,
  }],
});

const List = () => {
  const items = useRecoilValue(itemsState);

  return (
    {items.map((item, i) => (
      <div key={i}>
        {item.description}
      </div>
    ))}
  )
}

export default List;

Pretty straightforward right?

# Show me a selector

This is a dead-simple selector named unfinishedItemsState that pulls the unfinished items from itemsState atom:

import React from 'react';
import { atom, selector } from 'recoil';

const itemsState = atom({
  key: 'itemsState',
  default: [{
    description: 'Don\'t be lazy, write the post of the week ๐Ÿ˜ฌ',
    done: false,
  }],
});

const unfinishedItemsState = selector({
  key: 'unfinishedItemsState',
  get: ({ get }) => {
    const items = get(itemsState);

    return items.filter(item => item.done === false);
  }
});

As we mentioned above, selectors are pure functions, and we use them to pull data from atoms or even other selectors . They can get both of these as input, and they get re-evaluated every time state changes. The components that subscribe to them, re-render each time accordingly.

Let's create a more complex selector by using unfinishedItemsState selector:

import React from 'react';
import { atom, selector } from 'recoil';

const itemsState = atom({
  key: 'itemsState',
  default: [{
    description: 'Don\'t be lazy, write the post of the week ๐Ÿ˜ฌ',
    done: false,
  }],
});

const unfinishedItemsState = selector({
  key: 'unfinishedItemsState',
  get: ({ get }) => {
    const items = get(itemsState);

    return items.filter(item => item.done === false);
  }
});

const unfinishedItemsCountState = selector({
  key: 'unfinishedItemsCountState',
  get: ({ get }) => {
    const items = get(unfinishedItemsState);

    return items.length;
  }
});

We added a new one named unfinishedItemsCountState that is pulling unfinished items from unfinishedItemsState selector and returns their length. Easy right?

Let's use unfinishedItemsCountState in our component then:

import React from 'react';
import { atom, selector, useRecoilValue } from 'recoil';

const itemsState = atom({
  key: 'itemsState',
  default: [{
    description: 'Don\'t be lazy, write the post of the week ๐Ÿ˜ฌ',
    done: false,
  }],
});

const unfinishedItemsState = selector({
  key: 'unfinishedItemsState',
  get: ({ get }) => {
    const items = get(itemsState);

    return items.filter(item => item.done === false);
  }
});

const unfinishedItemsCountState = selector({
  key: 'unfinishedItemsCountState',
  get: ({ get }) => {
    const items = get(unfinishedItemsState);

    return items.length;
  }
});

const List = () => {
  const unfinishedItemsCount = useRecoilValue(unfinishedItemsCountState);
  const items = useRecoilValue(itemsState);

  return (
    <>
      You have {unfinishedItemsCount} unfinished tasks!!
      {items.map((item, i) => (
        <div key={i}>
          {item.description}
        </div>
      ))}
    </>
  );
}

export default List;

Awesome!!

# Ok, what about native React hooks then?

Nothing special here. We keep on using React hooks same as we did before in our components alongside RecoilJS hooks .

As we saw above, useRecoilValue hook returns the items list itself. How can we update this list then? We could use useRecoilState hook that exposes a method we can use to update the list accordingly.

Time to create a controlled input with the help of React.useState and then take advantage of useRecoilState hook to add new items to our list:

import React from 'react';
import { atom, selector, useRecoilValue, useRecoilState } from 'recoil';

const itemsState = atom({
  key: 'itemsState',
  default: [{
    description: 'Don\'t be lazy, write the post of the week ๐Ÿ˜ฌ',
    done: false,
  }],
});

const unfinishedItemsState = selector({
  key: 'unfinishedItemsState',
  get: ({ get }) => {
    const items = get(itemsState);

    return items.filter(item => item.done === false);
  }
});

const unfinishedItemsCountState = selector({
  key: 'unfinishedItemsCountState',
  get: ({ get }) => {
    const items = get(unfinishedItemsState);

    return items.length;
  }
});

const List = () => {
  const unfinishedItemsCount = useRecoilValue(unfinishedItemsCountState);
  const [items, setItems] = useRecoilState(itemsState);
  const [value, setValue] = React.useState('');

  const handleSubmit = e => {
    e.preventDefault();
    setItems(items.concat({
      description: value,
      done: false,
    }));

    setValue('');
  };

  return (
    <>
      You have {unfinishedItemsCount} unfinished tasks!!
      {items.map((item, i) => (
        <div key={i}>
          {item.description}
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          value={value}
          onChange={e => setValue(e.target.value)}
        />
        <button disabled={!value}>
          Add
        </button>
      </form>
    </>
  );
}

export default List;

So we created a controlled input by using React.useState and every time the form gets submitted we are using setItems method - provided by useRecoilState - to update the list in itemsState atom.

The nice thing here is that the counter with the unfinished tasks gets updated automatically whenever we add a new todo item and this is done actually with the minimum effort. Pretty amazing, right?

# Ok, so how all these fit together?

RecoilJS uses context to make these outstanding bindings, that is why we need to wrap our top-level App component with RecoilRoot. This is a context provider provided by RecoilJS and must be an ancestor of all components that use atoms and selectors:

import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';

import App from './App';

ReactDOM.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>,
  document.getElementById('root'),
);

For sure we can have multiple roots and each of them has its own atoms with distinct values.

When we have nested roots the inner ones mask entirely the outer ones.

# Things we need to pay attention to

Obviously RecoilJS makes React state management way easier with the API it provides but there are more we haven't touched yet and we should mention here:

  • Selectors provide a very powerful API with a getter and a setter. we might find ourselves using a setter rarely but this actually gives even more flexibility to return writeable state from our selectors:
import { atom, selector } from 'recoil';

const tempFahrenheit = atom({
  key: 'tempFahrenheit',
  default: 32,
});

const tempCelcius = selector({
  key: 'tempCelcius',
  get: ({ get }) => ((get(tempFahrenheit) - 32) * 5) / 9,
  set: ({ set }, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32),
});
  • Selectors can use Promises and pull data from an external resource in a dynamic fashion. Because of this we can run pretty complex logic with the minimum boilerplate:
import { atom, selector, useRecoilValue } from 'recoil';

const currentUserNameQuery = selector({
  key: 'CurrentUserName',
  get: async ({get}) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    return response.name;
  },
});

function CurrentUserInfo() {
  const userName = useRecoilValue(currentUserNameQuery);
  return <div>{userName}</div>;
}
  • We can use React.Suspense to take care of asynchronous selectors:
function MyApp() {
  return (
    <RecoilRoot>
      <React.Suspense fallback={<div>Loading...</div>}>
        <CurrentUserInfo />
      </React.Suspense>
    </RecoilRoot>
  );
}
  • We can wrap our components or parts of the application with an ErrorBoundary since selectors can throw errors when something goes south:
const currentUserNameQuery = selector({
  key: 'CurrentUserName',
  get: async ({get}) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    if (response.error) {
      throw response.error;
    }
    return response.name;
  },
});

function CurrentUserInfo() {
  const userName = useRecoilValue(currentUserNameQuery);
  return <div>{userName}</div>;
}

function MyApp() {
  return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>}>
          <CurrentUserInfo />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
}
  • RecoilJS treats navigation and state persistence as first class concepts which is absolutely magnificent

So, that was it. I am sure you have lots of questions so I highly propose to play with it in an actual codebase and see how it goes. Feel free to use this codesandbox I put together with the examples above. Cheers!!

You can read the official documentation if you need to go deeper with RecoilJS

Did you like this one?

I hope you really did...

Marios_thumb

Newsletter

Get notified about latest posts and updates once a week!!