🧸
ゆる〜っと理解 Zustand
State管理としてHookベースで開発体験が良いと噂のZustandを調査した時のまとめ
基本的な使い方
import { create, createSelectors } from 'zustand'
type State = {
firstName: string
lastName: string
}
type Action = {
updateFirstName: (firstName: State['firstName']) => void
updateLastName: (lastName: State['lastName']) => void
}
const usePersonStore = create<State & Action>((set) => ({
firstName: '',
lastName: '',
updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
updateLastName: (lastName) => set(() => ({ lastName: lastName })),
}))
Typescriptでは基本的に型を指定した方が良い。
combineによるStateの型推論
型定義が面倒であれば以下のように、middlewareのcombineを使うことが推奨される。大体はこれでうまくいくが、replace
や Object.keys
を使用する際など型不整合が起きることもあるので注意
import { create, ExtractState } from 'zustand'
import { combine } from 'zustand/middleware'
const useBearStore = create(
combine({ bears: 0 }, (set) => ({
increase: (by: number) => set((state) => ({ bears: state.bears + by })),
})),
)
// ExtractStateで型の抽出も可能
type BearState = ExtractState<typeof useBearStore>
ミドルウェアの活用
ストアの状態やアクションに対して、追加の処理を挟み込むことができる。
-
devtools
…Redux DevTools と統合しStateの変化をタイムラインで可視化 -
persist
…Stateをローカルストレージやセッションストレージに永続化- stateの一部だけを保存するときは
partialize
オプションを使う
- stateの一部だけを保存するときは
-
immer
…Stateの更新を簡易にする
Stateの更新でネストする場合はimmer
やoptics-ts
やRamda
を使う
https://zustand.docs.pmnd.rs/guides/updating-state#deeply-nested-object
複数Storeの管理
スライスパターンで複数のストアを1つに統合する。
import { create } from 'zustand'
const createFishSlice = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
// 統合
export const useBoundStore = create((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}))
// コンポーネント側での使用
import { useBoundStore } from './stores/useBoundStore'
function App() {
const bears = useBoundStore((state) => state.bears)
const fishes = useBoundStore((state) => state.fishes)
const addBear = useBoundStore((state) => state.addBear)
return (
<div>
<h2>Number of bears: {bears}</h2>
<h2>Number of fishes: {fishes}</h2>
<button onClick={() => addBear()}>Add a bear</button>
</div>
)
}
Selector
Storeからの計算されたStateをサブスクライブする場合はSelectorを使うのがおすすめ。下記の例では「セレクター=useMeals((state) => Object.keys(state))
」
不要な再レンダリングを防ぐためにはuseShallow
を使う
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
const useMeals = create(() => ({
papaBear: 'large porridge-pot',
mamaBear: 'middle-size porridge pot',
littleBear: 'A little, small, wee pot',
}))
export const BearNames = () => {
// ↓Selector
// useShallowは不要な再レンダリングを防ぐ
const names = useMeals(useShallow((state) => Object.keys(state)))
return <div>{names.join(', ')}</div>
}
https://zustand.docs.pmnd.rs/guides/prevent-rerenders-with-use-shallow
なるべく再利用性の高いものについてはcreateSelectors
を書くのがおすすめ
// Selector(使い所なるべく再利用性の高いものに限る)
// 以下はシンプルな例
const useBearStoreBase = create<BearState>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
increment: () => set((state) => ({ bears: state.bears + 1 })),
}))
const useBearStore = createSelectors(useBearStoreBase)
// get the property
const bears = useBearStore.use.bears()
// get the action
const increment = useBearStore.use.increment()
https://zustand.docs.pmnd.rs/guides/auto-generating-selector
副作用 ~StoreA の値の変化をトリガーに StoreB の値を変更したい~
Store依存の副作用をどう書くか調べたところ、以下のパターンのいずれかを採用すると良さそう。
- StoreA のアクション内
import { create } from 'zustand';
const useStoreA = create((set) => ({
valueA: 0,
setValueA: (newValue: number) => {
set({ valueA: newValue });
useStoreB.getState().setValueB(newValue * 2); // StoreB の値を変更
},
}));
const useStoreB = create((set) => ({
valueB: 0,
setValueB: (newValue: number) => set({ valueB: newValue }),
}));
export { useStoreA, useStoreB };
- カスタムミドルウェア
import { create, StoreApi, UseBoundStore } from 'zustand';
type Middleware<S extends object> = (
config: (set: StoreApi<S>['setState'], get: StoreApi<S>['getState'], api: StoreApi<S>) => S,
) => (set: StoreApi<S>['setState'], get: StoreApi<S>['getState'], api: StoreApi<S>) => S;
const syncStoreAWithStoreB: Middleware<any> = (config) => (set, get, api) => {
const initialState = config(set, get, api);
return {
...initialState,
setValueA: (newValue: number) => {
set((state) => ({ ...state, valueA: newValue }));
useStoreB.getState().setValueB(newValue * 2);
},
};
};
const useStoreA = create(syncStoreAWithStoreB((set) => ({
valueA: 0,
setValueA: (newValue: number) => set({ valueA: newValue }),
})));
const useStoreB = create((set) => ({
valueB: 0,
setValueB: (newValue: number) => set({ valueB: newValue }),
}));
export { useStoreA, useStoreB };
- React コンポーネント内での
useEffect
import React, { useEffect } from 'react';
import { useStoreA, useStoreB } from './stores';
const MyComponent = () => {
const valueA = useStoreA((state) => state.valueA);
useEffect(() => {
useStoreB.getState().setValueB(valueA * 2);
}, [valueA]);
return <div>...</div>;
};
export default MyComponent;
Discussion