ReactのZustandについて学習した。
Table of contents
Open Table of contents
Zustandとは
React向けの軽量な状態管理ライブラリ。 先に勉強したContext APIよりもシンプルに、コンポーネント間での状態を共有できる。
Contextとの違い
ZustandとContextの違いは、下記のようなものがある。
- Contextよりも簡潔に書ける。Providerで囲まなくて良い
- Context APIでは
Providerのvalueが更新されると、そのContextを利用しているコンポーネントが再レンダリングされる。一方Zustandは必要な状態だけを購読できる。
Zustandの使い方
基本パターン
状態を読む
const todos = useTodoStore((state) => state.todos);
関数を呼び出す
const addTodo = useTodoStore((state) => state.addTodo);
下記のように分割代入でまとめて呼び出すことも可能。
const { todos, addTodo } = useTodoStore();
ただし、store全体を取得するため、必要な値だけselectorで取得する書き方の方が再レンダリングを抑えやすい。
状態を更新する
set((state) => ({
todos: [...state.todos, newTodo],
}));
カウンターの作成
Zustandを使ったカウンターを作成。
useCountStoreを作成。ここでzustandのcreate関数を使って初期状態と更新関数を定義する。
CounterStore型を作成しておく。
関数の型は() => voidとして定義する。
import { create } from "zustand";
type CounterStore = {
count: number;
increment: () => void;
decrement: () => void;
};
export const useCountStore = create<CounterStore>((set) => ({
count: 0,
increment: () =>
set((state) => ({
count: state.count + 1
})),
decrement: () =>
set((state) => ({
count: state.count > 0 ? state.count - 1 : state.count
})),
}));
実際にカウントアップ・ダウンを行うCounterコンポーネントを作成。
ここでuseCountStoreを使ってカウントの値と更新関数を取得する。
定数count、increment、decrementにそれぞれ、useCountStoreで定義した値、関数を割り当てる。
記述方法は値も関数も基本的に同じ。
import { useCountStore } from "./useCountStore";
export const Counter = () => {
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
return (
<div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
CounterDisplayコンポーネントは、カウントの値を表示するだけのコンポーネント。
Counterコンポーネントと同様、useCountStoreを使ってcountの値を取得する。
これで別コンポーネントとcountの状態を共有することができる。
import { useCountStore } from "./useCountStore";
export const CounterDisplay = () => {
const count = useCountStore((state) => state.count);
return (
<div>
<p>Count Display: {count}</p>
</div>
);
};
定義した各コンポーネントを読み込み、Appコンポーネントで使用する。
import { Counter } from './Counter';
import { CounterDisplay } from './CounterDisplay';
function App() {
return (
<>
<CounterDisplay />
<Counter />
</>
);
}
export default App;
TODOリストの作成
Zustandを使ってTODOリストを作成する。
カウンター同様の手順でuseTodoStoreを作成する。
型は単体のTodo用のTodoと、Zustand用のTodoStoreを定義する。
todosの初期値は単体のTodoの配列で、addTodo、toggleTodo、deleteTodoの各アクションを定義する。
今回、idには簡易的にDate.now()を使用しているが、実運用ではuuidなどを使うことが多い。
import { create } from "zustand";
type Todo = {
id: number;
title: string;
completed: boolean;
};
type TodoStore = {
todos: Todo[];
addTodo: (title: string) => void;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
};
export const useTodoStore = create<TodoStore>((set) => ({
todos: [{ id: Date.now(), title: "test", completed: false }],
addTodo: (title) =>
set((state) => ({
todos: [
...state.todos,
{
id: Date.now(),
title,
completed: false,
}]
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
})),
deleteTodo: (id) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id)
})),
}));
Todoの入力を行うTodoInputを作成。
useStateを使って入力値を管理し、handleSubmit内でaddTodoアクションを呼び出す。
import { useTodoStore } from './useTodoStore';
import { useState } from 'react';
export function TodoInput() {
const [title, setTitle] = useState('');
const addTodo = useTodoStore((state) => state.addTodo);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
addTodo(title);
setTitle('');
};
return (
<form onSubmit={handleSubmit}>
<input type="text" value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit">Add</button>
</form>
);
}
Todoの表示を行うTodoListを作成。
useTodoStoreを使ってtodosを取得し、各Todoのcompleted、title、idを表示する。
toggleTodoとdeleteTodoアクションを呼び出すボタンを追加する。
import { useTodoStore } from './useTodoStore';
export function TodoList() {
const todos = useTodoStore((state) => state.todos);
const toggleTodo = useTodoStore((state) => state.toggleTodo);
const deleteTodo = useTodoStore((state) => state.deleteTodo);
return (
<>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} />
<span>{todo.title}</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</>
);
}
定義した各コンポーネントを読み込み、Appコンポーネントで使用する。
import { TodoList } from './TodoList';
import { TodoInput } from './TodoInput';
function App() {
return (
<>
<TodoInput />
<TodoList />
</>
);
}
export default App;
まとめ
ZustandはContextよりもシンプルに状態管理を行うことが可能。
上記で作成したuseTodoStoreやuseCountStoreで一括して状態と更新処理をまとめて管理できるため、コンポーネント間でのデータ受け渡しがシンプルになると感じた。
特にProviderが不要で、必要な状態だけ取得できる点は大きなメリットだと思う。