Zenn
🧸

ゆる〜っと理解 Zustand

に公開
1

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を使うことが推奨される。大体はこれでうまくいくが、replaceObject.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オプションを使う
  • immer…Stateの更新を簡易にする

Stateの更新でネストする場合はimmeroptics-tsRamdaを使う

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;
GitHubで編集を提案
1

Discussion

ログインするとコメントできます