[insides of computer]

Optimizing State Management In React With Recoil

As a developer in training, I have found React to be the best overall framework for handling the front end of web applications, and have become acquainted with the syntax required to manipulate state. Ultimately state is the medium in which data is shared between components, and the process of state management is often carried out by dedicated libraries outside of React for better data handling, such as Redux. Enter Recoil, an experimental state management library created by Facebook.

Recoil functions as a library, but keeps much of the same syntax as React. Moreover, it solves many of the problems regarding state, including global state management, shared state, and derived data issues, all while integrating seamlessly with the React API. Here I will introduce some of the key features of Recoil, and how to implement it into a project.

Motivations

For simple projects, there is nothing wrong with using React’s built-in state management system. However, issues begin to arise with more complex applications of the framework. For one, a component’s state can may be shared, but only pushing it up to the common ancestor, which often results in pushing up huge tree that then needs to re-render. In addition, a given context can only store one ascribed value, and not an indefinite set. These two issues combined may require code-splitting to resolve, in which the locations of where state is stored and where it is used, are separated.

Recoil offers a set of proposals to state management while keeping the semantics and behavior as similar to those of React as possible. The library accomplishes this as follows:

  • Recoil defines a directed graph, which is attached to the React tree. A directed graph, is simply a graph with a set defined nodes or vertices, which are connected together by edges which direct the data flow between vertices.
Example of a directed graph
  • State changes flow from atoms, or the roots of this directed graph, through selectors, or pure functions, into each component.

Using this system of state flow accomplishes a few tasks:

  • It results in a boilerplate-free API where shared state has the same simple get/set interface as React local state.
  • The state definition is incremental and distributed, allowing for code-splitting that is not too confusing.
  • State can be replaced with derived data without modifying the components that use it.
  • This derived data is such data which is able to move between being synchronous and asynchronous without modifying the components that are using it.
  • This structure allows for persistence of the entire application state in a way that survives large application changes.

The following image gives a rough illustration of how the directed graph, containing state data, ‘sits’ on a conventional component:

Recoil directed graph illustration

Installation

As Recoil is a state management library for React, it is necessary to have React already installed. Recoil.js may be installed directly from npm or yarn:

npm install recoil
//or
yarn add recoil

If using the npm method, Recoil it works works quite well when used with bundlers such as Webpack.

Major Concepts

Having already introduced how state flow is carried out with Recoil, we can now get into the specifics of the library’s two main features: atoms, and selectors.

Atoms

Atoms are the pieces of state that constitute the main source of truth of our application state. Also, atoms can be read from and written to from any component. They can be updated and subscribed. Whenever an atom is updated, all the subscribed components of it are re-rendered with a new value. Atoms may be created within a component by using an atom function, as follows:

const todoListState = atom({
key: 'todoListState',
default: [],
});

In the above example, for creation of a todo list, the source of truth will be an array of objects, with each object representing a todo item. To read the contents of this atom, we can use the useRecoilValue() hook in a TodoList component. To create new todo items, we would need to access a setter function that will update the contents of the todoListState. We can use the useSetRecoilState() hook to get a setter function in a TodoItemCreator component.

We use useRecoilState hook to read and write an atom from a component. Although it is similar to useState hook in React, you can also share the state between components.

Here is a code example of how our todoListState can be manipulated:

// TodoList function
function
TodoList() {
const todoList = useRecoilValue(todoListState);
return (
<>
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
}
// TodoList function creator:
function TodoItemCreator() {
const [inputValue, setInputValue] = useState('');
const setTodoList = useSetRecoilState(todoListState);
const addItem = () => {
setTodoList((oldTodoList) => [
...oldTodoList,
{
id: getId(),
text: inputValue,
isComplete: false,
},
]);
setInputValue('');
};
const onChange = ({target: {value}}) => {
setInputValue(value);
};
return (
<div>
<input type="text" value={inputValue} onChange={onChange} />
<button onClick={addItem}>Add</button>
</div>
);
}
// utility for creating unique Id
let id = 0;
function getId() {
return id++;
}

One last note: each atom requires a unique key for debugging, persistence, and mapping function.

Selectors

A selector is a piece of derived state, where ‘derived state’ can be defined as the ‘the output of passing state to a pure function that modifies the given state in some way’.

You can declare a new selector as given here:

const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
});

In the above example, the selector has the fontSizeState atom as its dependency. This selector behaves like a pure function that takes a fontSizeState as input and returns a formatted font size label.

Selectors also have a unique ID like atoms, but not a default value. A selector takes atoms or other selectors as input, and when these inputs are updated, the selector function is then re-evaluated.

A selector is mainly used to calculate state-based derived data in order to avoid any redundant states. This is accomplished because a minimal set of states is stored in the atoms and others are computed as a function of that minimal state. This whole approach is efficient because the selectors keep a record of components that need them, as well as the state state which they depend upon.

The get property of selectors is the function that is to be computed. It is able to access the value of atoms and other selectors using the get argument passed to it. Whenever it accesses another atom or selector, a dependency relationship is created such that updating the other atom or selector will cause this one to be recomputed.

To read selectors, we can use the hook useRecoilValue(), which takes an atom or selector as an argument and returns value associated with it. Selectors can be writeable or non-writeable. The following example uses a non-writeable selector, in creating a FontButton function:

function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
const fontSizeLabel = useRecoilValue(fontSizeLabelState);
return (
<>
<div>Current font size: {fontSizeLabel}</div>
<button onClick={() => setFontSize(fontSize + 1)} style={{fontSize}}>
Click to Enlarge
</button>
</>
);
}

In this example, clicking on the button accomplishes two tasks: it increases the font size of the button while also updating the font size label to reflect the current font size.

Conclusion

As mentioned before, for smaller or at least more straightforward projects, in which state management is not expected to be too complicated, there is nothing wrong with continuing to rely on React’s built-in state features. Otherwise, as a project grows in complexity and would be better served with an external library to handle state, Recoil is a great option, as it retains much of the semantics of React, and its syntax and features are relatively easy to learn. Furthermore, due to the increasing trend towards hooks, Recoil is even more valuable, particularly in contrast to other state management libraries such as Redux. Even if a project appears to be unlikely to run into state issues, it is still worthwhile to incorporate Recoil, as the library is full of useful tools that can help guide the developer towards maximizing the myriad features of a React application.

Thank you for reading!

Sources: