本記事の目的
- Zustand の最低限を理解する
- Zustand の導入ができるようになる
Zustand とは
Zustand(ズースタント) とは、状態(State)を管理するための JavaScript ライブラリです。
ボイラープレートが少なく、とてもシンプルな実装が特徴です。Hooks ベースで状態を管理する仕組みを提供しており、React に慣れている方であれば学習コストは低いです。
また、Zustand はとても軽量であることも特徴です。Redux(react-redux + @reduxjs/toolkit)や Recoil が約 20 kB であるのに対して、Zustand は約 0.6 kB です。
ちなみに「Zustand」は、ドイツ語で「状態」という意味です。
導入手順
アプリケーションで Zustand を利用できるようにする手順を紹介します。とてもシンプルです。
インストール
Zustand をインストールします。
npm install zustand
Store を作成する
Zustand では、create を使用してカスタムフックを定義することで Store を作成することができます。
create には、管理したい状態(例: count)と、アクション(例: increase、decrease)をまとめたオブジェクトを返すコールバック関数を渡します。
コールバック関数の引数には、状態を更新する set 関数が渡されます。アクション内では、これを使って状態を更新することができます。
set 関数の引数には、最新の状態が渡されます。
// src/store/useCounterStore.ts
import { create } from "zustand";
interface CounterStore {
count: number;
increase: () => void;
decrease: () => void;
}
export const useCounterStore = create<CounterStore>((set) => ({
count: 0, // 管理したい状態
increase: () => set((state) => ({ count: state.count + 1 })), // アクション1
decrease: () => set((state) => ({ count: state.count - 1 })), // アクション2
}));
これで、Zustand で Store を使用する準備が整いました 👏✨
Redux と比較すると、Reducer や Action を別々に定義する必要もなく、Provider の設置も不要でとてもシンプルですね。
使用方法
定義した Store を、コンポーネント内で使用する手順を紹介します。
作成したカスタムフック(例: useCounterStore)を、コンポーネントから使用することで Store を利用することができます。
カスタムフックのコールバック関数で、参照したい状態またはアクション名を返してもらう形で使用します。
useCounterStore((state) => state.<参照したい状態 または アクション>);
import { useCounterStore } from "./store/useCounterStore";
function Counter() {
const count = useCounterStore((state) => state.count);
const increase = useCounterStore((state) => state.increase);
const decrease = useCounterStore((state) => state.decrease);
return (
<>
<h1>Count: {count}</h1>
<button onClick={increase}>+</button>
<button onClick={decrease}>−</button>
</>
);
}
【Tips】 Store 使用時に分割代入を使用した際の挙動について
Store を使用する際には、以下のように分割代入を用いて状態やアクションを取得しても、基本的には問題なく動作します。
記述も少なく見やすいというメリットがありますが、実装によっては不要な再レンダリングが発生してしまうため、推奨されません。
const { count, increase, decrease } = useCounterStore();
例えば、同じ Store 内で count と label の 2 つの状態を管理している場合を想定します。
フォームを入力して label を更新した際、本来であれば label を使用している Label コンポーネントのみ再レンダリングされるべきです。
しかし、label を使用していない Counter コンポーネントも一緒に再レンダリングされてしまいます。
コンソールには「Label component re-rendered.」と「Counter component re-rendered.」の両方が出力されることを確認できます。
このように、使用していない状態が更新された際にも不要に再レンダリングされてしまう のが、分割代入を使用する際のデメリットです。
const useSampleStore = create((set) => ({
count: 0, // 状態(1)
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
label: "Hello", // 状態(2)
setLabel: (label: string) => set({ label }),
}));
function Label() {
const { label, setLabel } = useSampleStore();
console.log("Label component re-rendered.");
return (
<>
<h1>Label: {label}</h1>
<input value={label} onChange={(e) => setLabel(e.target.value)} />
</>
);
}
function Counter() {
const { count, increase, decrease } = useSampleStore();
console.log("Counter component re-rendered.");
return (
<>
<h1>Count: {count}</h1>
<button onClick={increase}>+</button>
<button onClick={decrease}>−</button>
<Label />
</>
);
}
一方、分割代入を使用せずに実装してみると、label の更新時には Label コンポーネントだけが再レンダリングされ、Counter コンポーネントは再レンダリングされません。
コンソールには「Label component re-rendered.」のみが出力されることを確認できます。
やや冗長感は否めませんが、Store を使用する際には分割代入は使用せずに以下のように記述することを推奨します。
function Label() {
const label = useSampleStore((s) => s.label);
const setLabel = useSampleStore((s) => s.setLabel);
console.log("Label component re-rendered.");
//...
}
function Counter() {
const count = useSampleStore((s) => s.count);
const increase = useSampleStore((s) => s.increase);
const decrease = useSampleStore((s) => s.decrease);
console.log("Counter component re-rendered.");
//...
}