はじめに
zustandはコンパクトなReactで動作する状態管理ライブラリです。
ミドルウェアで必要な分だけ機能拡張も可能です。
Redux、Recoilなど他の状態管理ライブラリと比較すると
最初に覚える概念が少なくContext.Providerでラップしなくてよかったりと
導入しやすいライブラリだと思います。
今回はzustandを使ってみたまとめになります。
インストール
npm install zustand
基本的な使い方
ストアを作成してそれを呼び出すだけなのでとても簡単です。
ストア作成
interface BearState {
bears: number
increase: () => void
}
export const useBearStore = create<BearState>()((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
}))
コンポーネントにバインド
function Controls() {
const increase = useBearStore((state) => state.increase)
return <button onClick={increase}>one up</button>
}
複数の状態を持っている場合の取得
個別に取得
基本的には必要な状態のみ取得するようにします。
そうすることで不要な再レンダリングを防げます。
const nuts = useBearStore((state) => state.nuts)
const honey = useBearStore((state) => state.honey)
複数の状態を同時に取得
ネストしていないフラットな複数の状態を同時に取得する場合はshallowを指定することで
指定されていない状態が更新されたときの再レンダリングを防ぐことが出来ます。
shallowを指定しない場合useBearStoreにbearsというもう一つの状態があり更新されたとき
下記も再レンダリングされます。
const { nuts, honey } = useBearStore(
(state) => ({ nuts: state.nuts, honey: state.honey }),
shallow
)
再レンダリングをより詳細に制御するには、任意のカスタム等値関数を作成します。
const treats = useBearStore(
(state) => state.treats,
(oldTreats, newTreats) => compare(oldTreats, newTreats)
)
状態の更新
ReactのuseStateと同様に自動的な更新オブジェクトのマージを行いません。
下記のuseStateの例のようにマージしたい場合、スプレッド構文と併用します。
zustandのset関数も同様に状態を保持する必要があります。
const [state, setState] = useState({});
setState(prevState => {
return {...prevState, ...updatedValues};
});
zustandでストアを作成する時の典型的な例では下記のような形になります
import { create } from 'zustand'
const useCountStore = create((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
set関数は保持している値にマージするために本来はこのような形になるはずです。
set((state) => ({ ...state, count: state.count + 1 }))
ただし、これは一般的なパターンであるため
...state部分をスキップできるので最終的に下記だけになります。
set((state) => ({ count: state.count + 1 }))
ネストされたオブジェクト
set関数は、1つのレベルでのみ状態をマージします。
ネストされたオブジェクトがある場合は、それらを明示的にマージする必要があります。
例えば次のようにスプレッド演算子を使用します。
import { create } from 'zustand'
const useCountStore = create((set) => ({
nested: { count: 0 },
inc: () =>
set((state) => ({
nested: { ...state.nested, count: state.nested.count + 1 },
})),
}))
さらに深くネストした状態を持っている場合はスプレッド演算子を各レベルで書く必要があり
冗長でコードが長くなるのでimmerミドルウェア等を利用します。
ベストプラクティス
1つの大きなストアを作りその中に小さなストアを設置するパターンが推奨されています。
export const createFishSlice = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
export const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
import { create } from 'zustand'
import { createBearSlice } from './bearSlice'
import { createFishSlice } from './fishSlice'
export const useBoundStore = create((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}))
まとめ
zustandは簡単に導入出来てとても使いやすいと感じました。
気軽に導入出来る分、使い方を工夫しないと予期しない状態変更になったりすると思うので
プロジェクト内で使い方を予め定義して導入すると良さそうです。
引き続きzustandの情報収集はやっていきたいと思っています。