Next.js Recoil
Introduction
Recoil is a state management library for React applications created by Facebook. It provides a way to share state between components that's more efficient and easier to use than Context API for complex state scenarios. In this guide, we'll explore how to integrate and use Recoil with Next.js to manage application state effectively.
Recoil introduces a few key concepts:
- Atoms: Basic units of state that components can subscribe to
- Selectors: Derived state that can transform atoms
- Hooks:
useRecoilState
,useRecoilValue
, and more for accessing and updating state
Let's dive into setting up and using Recoil in your Next.js application.
Setting Up Recoil in Next.js
1. Installation
First, install Recoil in your Next.js project:
npm install recoil
# or
yarn add recoil
2. Setting up Recoil Root
Recoil requires the RecoilRoot
component to wrap your application. In Next.js, you need to add this to your _app.js
file:
// pages/_app.js
import { RecoilRoot } from 'recoil';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return (
<RecoilRoot>
<Component {...pageProps} />
</RecoilRoot>
);
}
export default MyApp;
This ensures that Recoil state is accessible throughout your entire application.
Core Concepts of Recoil
Atoms
Atoms are units of state in Recoil. They're like individual pieces of your application state that components can subscribe to. When an atom's value changes, all components that use that atom will re-render.
Let's create a simple atom:
// atoms/counterAtom.js
import { atom } from 'recoil';
export const counterState = atom({
key: 'counterState', // unique ID (with respect to other atoms/selectors)
default: 0, // default value
});
The key
must be unique across your entire application, as it's used internally by Recoil to identify the atom.
Using Atoms in Components
Now let's use this atom in a component:
// components/Counter.js
import { useRecoilState, useRecoilValue } from 'recoil';
import { counterState } from '../atoms/counterAtom';
function Counter() {
// Similar to useState, but state is shared across components
const [count, setCount] = useRecoilState(counterState);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
// If you only need to read the state without updating it
function CountDisplay() {
const count = useRecoilValue(counterState);
return <div>Current count: {count}</div>;
}
export { Counter, CountDisplay };
Selectors
Selectors represent derived state - computed values based on atoms or other selectors. They're perfect for transforming or filtering data.
// selectors/counterSelectors.js
import { selector } from 'recoil';
import { counterState } from '../atoms/counterAtom';
export const doubledCountState = selector({
key: 'doubledCountState',
get: ({ get }) => {
const count = get(counterState);
return count * 2;
},
});
export const isEvenState = selector({
key: 'isEvenState',
get: ({ get }) => {
const count = get(counterState);
return count % 2 === 0;
},
});
Using selectors in components:
// components/CounterStats.js
import { useRecoilValue } from 'recoil';
import { doubledCountState, isEvenState } from '../selectors/counterSelectors';
function CounterStats() {
const doubledCount = useRecoilValue(doubledCountState);
const isEven = useRecoilValue(isEvenState);
return (
<div>
<p>Doubled count: {doubledCount}</p>
<p>Count is {isEven ? 'even' : 'odd'}</p>
</div>
);
}
export default CounterStats;
Advanced Usage: Async Selectors
Recoil can handle asynchronous data with async selectors, perfect for API calls:
// atoms/userAtom.js
import { atom, selector } from 'recoil';
export const userIdState = atom({
key: 'userIdState',
default: 1,
});
export const userDataState = selector({
key: 'userDataState',
get: async ({ get }) => {
const userId = get(userIdState);
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return response.json();
},
});
Using async selectors in components:
// components/UserProfile.js
import { useRecoilState, useRecoilValue } from 'recoil';
import { userIdState, userDataState } from '../atoms/userAtom';
import { Suspense } from 'react';
function UserData() {
const userData = useRecoilValue(userDataState);
return (
<div>
<h3>{userData.name}</h3>
<p>Email: {userData.email}</p>
<p>Phone: {userData.phone}</p>
</div>
);
}
function UserProfile() {
const [userId, setUserId] = useRecoilState(userIdState);
return (
<div>
<h2>User Profile</h2>
<select
value={userId}
onChange={(e) => setUserId(Number(e.target.value))}
>
{[1, 2, 3, 4, 5].map(id => (
<option key={id} value={id}>User {id}</option>
))}
</select>
<Suspense fallback={<div>Loading user data...</div>}>
<UserData />
</Suspense>
</div>
);
}
export default UserProfile;
Practical Example: Todo List with Recoil
Let's create a more complete example with a todo list application:
// atoms/todoAtoms.js
import { atom, selector } from 'recoil';
export const todoListState = atom({
key: 'todoListState',
default: [],
});
export const todoFilterState = atom({
key: 'todoFilterState',
default: 'all', // 'all', 'completed', 'incomplete'
});
export const filteredTodoListState = selector({
key: 'filteredTodoListState',
get: ({ get }) => {
const filter = get(todoFilterState);
const list = get(todoListState);
switch (filter) {
case 'completed':
return list.filter((item) => item.completed);
case 'incomplete':
return list.filter((item) => !item.completed);
default:
return list;
}
},
});
export const todoStatsState = selector({
key: 'todoStatsState',
get: ({ get }) => {
const todoList = get(todoListState);
const totalNum = todoList.length;
const totalCompleted = todoList.filter((item) => item.completed).length;
const totalIncomplete = totalNum - totalCompleted;
const percentCompleted = totalNum === 0 ? 0 : (totalCompleted / totalNum) * 100;
return {
totalNum,
totalCompleted,
totalIncomplete,
percentCompleted,
};
},
});
Now let's create the components for our Todo app:
// components/TodoList.js
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { todoListState, todoFilterState, filteredTodoListState, todoStatsState } from '../atoms/todoAtoms';
function TodoItemCreator() {
const [inputValue, setInputValue] = useState('');
const [todoList, setTodoList] = useRecoilState(todoListState);
const addTodo = () => {
if (!inputValue.trim()) return;
setTodoList([
...todoList,
{
id: Date.now(),
text: inputValue,
completed: false,
},
]);
setInputValue('');
};
return (
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<button onClick={addTodo}>Add</button>
</div>
);
}
function TodoItem({ item }) {
const [todoList, setTodoList] = useRecoilState(todoListState);
const toggleItemCompletion = () => {
const newList = todoList.map((listItem) =>
listItem.id === item.id
? { ...listItem, completed: !listItem.completed }
: listItem
);
setTodoList(newList);
};
const deleteItem = () => {
const newList = todoList.filter((listItem) => listItem.id !== item.id);
setTodoList(newList);
};
return (
<div>
<input
type="checkbox"
checked={item.completed}
onChange={toggleItemCompletion}
/>
<span style={{ textDecoration: item.completed ? 'line-through' : 'none' }}>
{item.text}
</span>
<button onClick={deleteItem}>Delete</button>
</div>
);
}
function TodoListFilters() {
const [filter, setFilter] = useRecoilState(todoFilterState);
return (
<div>
Filter:
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="completed">Completed</option>
<option value="incomplete">Incomplete</option>
</select>
</div>
);
}
function TodoListStats() {
const { totalNum, totalCompleted, totalIncomplete, percentCompleted } = useRecoilValue(todoStatsState);
return (
<ul>
<li>Total items: {totalNum}</li>
<li>Completed: {totalCompleted}</li>
<li>Not completed: {totalIncomplete}</li>
<li>Percent completed: {Math.round(percentCompleted)}%</li>
</ul>
);
}
function TodoList() {
const todoList = useRecoilValue(filteredTodoListState);
return (
<div>
<h1>Todo List</h1>
<TodoListStats />
<TodoListFilters />
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</div>
);
}
export default TodoList;
Using Recoil with Next.js SSR
Next.js uses server-side rendering (SSR), which can create challenges with client-side state management libraries like Recoil. Here's how to handle it:
// pages/todos.js
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
// Use dynamic import with SSR disabled for components that use Recoil
const TodoList = dynamic(() => import('../components/TodoList'), {
ssr: false,
});
export default function TodosPage() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return (
<div>
<h1>Todo Application</h1>
{isClient ? <TodoList /> : <div>Loading...</div>}
</div>
);
}
This approach ensures that Recoil code only runs on the client side, avoiding SSR-related issues.
Organizing Recoil State
For larger applications, it's a good practice to organize your Recoil state into modules:
/atoms
/user
userState.js
userSelectors.js
/todos
todoState.js
todoSelectors.js
/ui
uiState.js
This structure helps keep your code maintainable as your application grows.
Summary
In this guide, we've covered:
- Setting up Recoil in a Next.js application
- Creating and using atoms for global state
- Using selectors for derived state
- Working with asynchronous data
- Building a practical todo list application
- Handling SSR with Recoil
Recoil offers a powerful, flexible state management solution that integrates well with Next.js applications. Its atom-based approach makes it easier to reason about state, while selectors provide powerful ways to transform and derive data.
Additional Resources
Exercises
- Extend the todo list application to include priorities (high, medium, low)
- Create a shopping cart application using Recoil
- Implement a theme switcher (dark/light mode) that persists across page reloads
- Build a multi-step form wizard that maintains state between steps
- Create a dashboard with multiple widgets that share data through Recoil
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)