State Management with Recoil

Recoil is a relatively new state management library specifically built for React applications by Facebook. It is designed to be simple yet powerful, allowing you to manage both local and global state efficiently while solving problems like prop drilling and complex data flows.

What is Recoil?

Recoil is a state management library for React that provides a novel way to share state across components while keeping the data flow manageable and predictable. Recoil enables fine-grained state management and comes with a few unique features:

  • Atoms: The units of state in Recoil that can be shared across components.
  • Selectors: Derived or computed state that can depend on other atoms or selectors.
  • Dependency Graph: Recoil automatically manages dependencies between atoms and selectors, ensuring that only the relevant parts of your app re-render when state changes.

Why Use Recoil for State Management?

Recoil offers several advantages over other state management libraries:

  1. Minimal Boilerplate: Compared to libraries like Redux, Recoil requires significantly less setup and code to get started.
  2. Fine-grained Reactivity: Recoil allows for more efficient reactivity by ensuring that only the components depending on specific atoms or selectors re-render when those values change.
  3. Derived State: Recoil provides built-in support for computed state via selectors.
  4. Concurrency Support: Recoil is designed with concurrent React features in mind, making it future-proof for applications that leverage React Suspense or Concurrent Mode.

Installing and Setting Up Recoil

Before using Recoil in your React application, you need to install the Recoil library.

Installing Recoil

Run the following command in your project directory to install Recoil:

				
					npm install recoil

				
			

Setting Up Recoil

Once installed, you need to wrap your application with the RecoilRoot component. This component acts as the context provider for your Recoil state.

				
					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')
);

				
			

Explanation:

  • RecoilRoot: This component wraps the entire application to give access to the Recoil state across all components. It’s similar to how you would wrap your app with Redux’s Provider or React Context.

Atoms in Recoil

Atoms are the smallest units of state in Recoil. They are used to store pieces of state that can be shared between multiple components.

Creating an Atom

Let’s start by creating a simple atom to store a piece of state, such as a counter value.

				
					import { atom } from 'recoil';

export const counterState = atom({
  key: 'counterState', // A unique key to identify this atom
  default: 0, // The default value for this atom
});

				
			

Explanation:

  • atom: This function creates a new Recoil atom.
  • key: Each atom requires a unique key that identifies it in the Recoil state tree.
  • default: The initial value of the atom, in this case, it’s set to 0.

Using an Atom in a Component

Now that we’ve created the counterState atom, let’s use it in a component to display and update the counter value.

				
					import React from 'react';
import { useRecoilState } from 'recoil';
import { counterState } from './atoms';

const Counter = () => {
  const [count, setCount] = useRecoilState(counterState);

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
};

export default Counter;

				
			

Explanation:

  • useRecoilState: This hook is used to read and update the value of an atom. It works similarly to React’s useState, but it allows the state to be shared across multiple components.
  • count: The current value of the counterState atom.
  • setCount: A setter function to update the value of the counterState atom.

Output:

When you run the app, you’ll see a counter displayed with “Increment” and “Decrement” buttons. Clicking these buttons will update the counter, and the UI will re-render to reflect the updated value.

Selectors in Recoil

Selectors in Recoil allow you to create derived state. They can depend on atoms or other selectors and are recalculated whenever their dependencies change.

Creating a Selector

Let’s create a selector that doubles the value of our counter.

				
					import { selector } from 'recoil';
import { counterState } from './atoms';

export const doubledCounterState = selector({
  key: 'doubledCounterState', // Unique key for this selector
  get: ({ get }) => {
    const count = get(counterState); // Get the current value of counterState
    return count * 2;
  },
});

				
			

Explanation:

  • selector: This function creates a new selector.
  • get: The get function is used to read the value of other atoms or selectors.
  • counterState: We’re using the counterState atom and multiplying its value by 2.

Using a Selector in a Component

Let’s display the doubled value of the counter in our component.

				
					import React from 'react';
import { useRecoilValue } from 'recoil';
import { doubledCounterState } from './selectors';

const DoubledCounter = () => {
  const doubledCount = useRecoilValue(doubledCounterState);

  return (
    <div>
      <h2>Doubled Counter: {doubledCount}</h2>
    </div>
  );
};

export default DoubledCounter;

				
			

Explanation:

  • useRecoilValue: This hook is used to read the value of an atom or selector without subscribing to changes. In this case, we use it to get the doubledCounterState value.

Output:

The component will display the doubled value of the counter. If the original counter is 2, this component will show 4. When the counter value is updated, the doubled value will automatically be recalculated and updated in the UI.

Asynchronous Selectors (Handling API Requests)

Recoil selectors can also be asynchronous, allowing you to handle API requests or perform other asynchronous tasks.

Creating an Asynchronous Selector

Let’s create a selector that fetches data from an API (for example, user data from a public API).

				
					import { selector } from 'recoil';

export const userDataState = selector({
  key: 'userDataState',
  get: async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
    const data = await response.json();
    return data;
  },
});

				
			

Explanation:

  • get: The get function in a selector can return a promise, making it asynchronous.
  • fetch: We’re fetching data from a public API (JSONPlaceholder) to get information about a user.

Using Asynchronous Selectors in a Component

Now, let’s use this asynchronous selector to display user data in a component.

				
					import React from 'react';
import { useRecoilValue } from 'recoil';
import { userDataState } from './selectors';

const User = () => {
  const userData = useRecoilValue(userDataState);

  return (
    <div>
      <h1>User Data</h1>
      <p>Name: {userData.name}</p>
      <p>Email: {userData.email}</p>
      <p>Phone: {userData.phone}</p>
    </div>
  );
};

export default User;

				
			

Explanation:

  • useRecoilValue: We use this hook to read the value of the userDataState selector, which triggers the API request and renders the user data once it is available.

Output:

The component will display user information fetched from the API, such as the user’s name, email, and phone number.

Atoms with Complex State

In many cases, you will need to manage complex state such as arrays or objects. Recoil atoms can handle these scenarios efficiently.

Managing a Todo List

Let’s create a simple todo list application where we store the todos in a Recoil atom.

				
					import { atom } from 'recoil';

export const todoListState = atom({
  key: 'todoListState',
  default: [], // Default value is an empty array
});

				
			

Adding and Displaying Todos

Now let’s create a component to add new todos and display the list of todos.

				
					import React, { useState } from 'react';
import { useRecoilState } from 'recoil';
import { todoListState } from './atoms';

const TodoList = () => {
  const [todos, setTodos] = useRecoilState(todoListState);
  const [newTodo, setNewTodo] = useState('');

  const addTodo = () => {
    setTodos([...todos, newTodo]);
    setNewTodo('');
  };

  return (
    <div>
      <h1>Todo List</h1>
      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
        placeholder="Enter a new todo"
      />
      <button onClick={addTodo}>Add Todo</button>

      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

export default TodoList;

				
			

Explanation:

  • useRecoilState: We use this hook to manage the todoListState atom, which stores the array of todos.
  • addTodo: This function adds a new todo to the list by updating the atom.

Output:

You can add todos using the input field, and the list will display all added todos. The state of the todo list is stored in the todoListState atom, and it’s shared across all components that access this atom.

Recoil and React Suspense

Recoil is designed to work seamlessly with React Suspense, allowing you to handle asynchronous data fetching more elegantly. When an asynchronous selector is loading, Recoil can trigger the Suspense boundary and show a fallback UI until the data is ready.

Using Suspense with Recoil

Let’s wrap our asynchronous User component in a Suspense boundary to handle the loading state.

				
					import React, { Suspense } from 'react';
import User from './User';

const App = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <User />
    </Suspense>
  );
};

export default App;

				
			

Explanation:

  • Suspense: We wrap the User component with Suspense and provide a fallback UI (e.g., “Loading…”) that will be displayed while the data is being fetched.

Output:

When you load the app, you’ll see the fallback UI (“Loading…”) until the user data is fetched, at which point the actual data will be displayed.

Advanced Recoil: Resetting State

Recoil provides utilities to reset the state of atoms. This can be useful when you want to clear or reset state back to its default value.

Resetting Atom State

Let’s modify our todo list example to add a button that resets the todo list.

				
					import React from 'react';
import { useRecoilState, useResetRecoilState } from 'recoil';
import { todoListState } from './atoms';

const TodoList = () => {
  const [todos, setTodos] = useRecoilState(todoListState);
  const resetTodos = useResetRecoilState(todoListState);

  return (
    <div>
      <h1>Todo List</h1>
      <button onClick={resetTodos}>Reset Todos</button>

      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

export default TodoList;

				
			

Explanation:

  • useResetRecoilState: This hook provides a function to reset the value of an atom back to its default value.

Output:

Clicking the “Reset Todos” button will clear the todo list and reset it to an empty array.

Recoil is a powerful and flexible state management library for React applications. It provides a minimalistic and declarative way to manage both local and global state in React. With features like atoms, selectors, and built-in support for async state, Recoil makes it easy to handle complex state management scenarios, while keeping your code clean and efficient. Happy Coding!❤️

Table of Contents