zustandをもっと使いやすくするためのcustom hook戦略
はじめに
非同期状態管理(SWR/Tanstack Query)で大体は出来そうだなぁということでClient側での状態管理をそこまで深く考えてなかったのですが横断的に管理したいなぁという事象が例に漏れずでてきたので最初はContextAPIでやるかぁ🤔と思っていたのですが、拡張していくとprovider波動拳≒if波動拳になることも予想できたので他にないかなぁということで検討しており
結果としてzustandを使うことにしました。
providerでも沢山作るとこんな感じで波動拳打てるので。。。
調査していく中で公式や以下の非常にわかりやすく纏めてくださっている
記事がとても参考になりました🙇
公式に沿ったシンプルな形での導入でも良いのですが、ちょっと個人的には使い勝手が悪いなぁと思うことがあったのでGPTや師匠に相談しながらよりプロダクトにフィットする方法でのcustom hook戦略を考えたので備忘録も兼ねて以下に残しております。
結論
directory構造
store
├── slice
│ └── xxxxSlice.ts
│ └── xxxxSlice.ts
└── types
│ └── index.ts
└── index.ts
store/index.ts
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { createWithEqualityFn } from 'zustand/traditional';
import { createXXXXSlice } from './slice/xxxxSlice';
import { createXXXXSlice } from './slice/xxxxSlice';
import type { Store } from './types';
export const useStore = createWithEqualityFn<Store>()(
devtools(
persist(
subscribeWithSelector(
immer((...a) => ({
...createXXXXSlice(...a),
...createXXXXSlice(...a),
}))
),
{
name: 'local-storage',
}
)
)
);
store/types/index.ts
import { xxxxSlice } from '../slice/xxxxSlice';
import { xxxxSlice } from '../slice/xxxxSlice';
export type Store = xxxxSlice;
sampleとして時間を横断的に扱う場合の例としてsliceをdateSlice.tsとして定義しております。
(今回、App内で日付情報を局所的に同一状態のものとして持っておきたかったので)
import { addMonths, subMonths } from 'date-fns';
import { StateCreator } from 'zustand';
type DateState = {
date: Date;
};
type DateActions = {
goToday: () => void;
goNextMonth: () => void;
goPrevMonth: () => void;
};
export type DateSlice = DateState & DateActions;
const initialState: DateState = {
date: new Date(),
};
export const createDateSlice: StateCreator<
DateSlice,
[['zustand/immer', never]],
[],
DateSlice
> = (set) => ({
...initialState,
goToday: () => set({ date: new Date() }),
goNextMonth: () => set({ date: subMonths(initialState.date, 1) }),
goPrevMonth: () => set({ date: addMonths(initialState.date, 1) }),
});
// selector
export const dateSelector = (state: DateSlice) => ({
date: state.date,
goToday: state.goToday,
goNextMonth: state.goNextMonth,
goPrevMonth: state.goPrevMonth,
});
custome hookとしてuseStoreをhooks内に定義
import { shallow } from 'zustand/shallow';
import { useStoreWithEqualityFn } from 'zustand/traditional';
import { useStore as useZustandStore } from '../store';
const useStore = <U>(selector: (state: any) => U): U => {
return useStoreWithEqualityFn(useZustandStore, selector, shallow);
};
export default useStore;
import useStore from '@/app/hooks/useStore';
import { dateSelector } from '@/app/store/slice/dateSlice';
// componentやそれぞれのhooks内から以下の様に参照が可能になります。
// zustandのuseStoreではなくWrapperとして作った方を使うようにします。
~ 略 ~
const { date } = useStore(dateSelector);
型情報の参照について
解説
store/index.ts
の解説は上記にも引用させて頂きましたが以下の記事がすごく丁寧にまとめてくださっており、そちらを参考にして頂いた方が早いので割愛させて頂きます。
※特にimmer
やdevtools
など
useStore.ts(custom版)について
このコードは、Zustandの状態管理ライブラリを使ってカスタムフックを定義し、選択した状態を効率的に取得するためのものです。以下に各部分の詳細な説明をします。
import { shallow } from 'zustand/shallow';
import { useStoreWithEqualityFn } from 'zustand/traditional';
import { useStore as useZustandStore } from '../store';
shallow
Zustandのヘルパー関数で、浅い比較を行います。これにより、オブジェクトのプロパティが変わらない限り、再レンダリングを防ぐことができます。
useStoreWithEqualityFn
Zustandのフックで、選択された状態に基づいて比較関数を使って状態を取得します。
この場合、浅い比較関数(shallow)を使用します。
useStore as useZustandStore
他のモジュールからインポートしたZustandストアのフックを、useZustandStoreという別名で使用しております。
const useStore = <U>(selector: (state: any) => U): U => {
return useStoreWithEqualityFn(useZustandStore, selector, shallow);
};
useStore関数
ジェネリック型<U>を受け取るカスタムフックで、selector関数を引数に取ります。
selector関数は、ストアの状態から特定の部分を選択するために使用されます。
useStoreWithEqualityFnの呼び出し
useStoreWithEqualityFnを使用して、useZustandStoreから状態を取得します。
selector関数を使用して必要な状態を選択し、shallow関数を使って浅い比較を行います。
コード解説のまとめ
このカスタムフックuseStoreは、Zustandのストアから特定の状態を効率的に取得するために設計されています。浅い比較(shallow)を使用することで、選択された状態が実際に変更されたときのみコンポーネントを再レンダリングするように最適化されています。これにより、パフォーマンスの向上が期待できます。
なぜ、この作りにしたかったのか?
- フロントエンドとしてのcontext(≒domain)毎にsliceを作ることで無駄な参照を減らせる。
- zustandを使う意義でもあります。
- 公式にあるような形で状態・状態変更関数を
state.xxx.yyy
という方法で取得したくなかった。- 参照までのメソッドチェーンが長くなることでコード量が増え、可読性が落ちるのが嫌だった。
- selectorを引数に使うことで何(のcontext)を参照しているのかひと目でわかるようにしたかった。
- context毎にuseXXXStoreを作成し、importして使う方法を最初は考えていましたが元になるwrapperは一つである方が楽だなぁと思い最終的にこの方法になりました。
メリット
コード解説部分にもありますが、zustandの特徴でもあるパフォーマンス部分を損なわず使い勝手もいい形で参照できるようになりました。
デメリット??
そもそもxxxSliceを沢山作るつもりはないので(大体をTanstack Queryの方に寄せることになりそうなので)
ちょっと大げさかな🤔と...思ったり、思わなかったりしてます。
まとめ
zustandは、はじめて触ったのですが扱いやすくてとても良さそうです。
そんなに複雑なことをzustand側ではさせず薄く使う場合の参考にしていただけると嬉しいです!!
今はこちらの発売が楽しみです(波動拳はこちらで打ちたいですw)
引用
参考にさせて頂きました。
ありがとうございました🙇
Discussion