0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zustandの基本構成とスライスパターンをコードベースで解説

Posted at

動機

業務では状態管理にreduxを使うことがほとんどなのですが、正直reduxは面倒くさいことが多いです。

stateを1つ追加するだけでも書かなければならないコードが多く、学習コストもそれなりにかかるため、新しくプロジェクトにジョインしてくれた人に教育をするのも大変です。

何か別の状態管理ライブラリはないものかと探していた時に、Zustandを発見しました。

Zustandとは

Zustandは、React用の軽量な状態管理ライブラリです。開発したのは、CSS-in-JSライブラリ「emotion」や、通知ライブラリ「react-toastify」などで知られる、Poimandresというチームです。

Zustandの特徴は、「少ないコードでシンプルに状態を管理できる」という点です。Reduxと比べると、非常に直感的で、設定にかかる手間がほとんどありません。

公式

使い方

インストール

npm install zustand

もしくは

yarn add zustand

基本的なStoreの構成

import { create } from 'zustand'

const useStore = create((set) => ({
  bears: 0,
  increase: () => set((state) => ({ bears: state.bears + 1 })),
  decrease: () => set((state) => ({ bears: state.bears - 1 })),
}))

Storeはcreate関数を使って作成します。bearsに加えて、これを変更するアクションも同じ場所で管理されています。

続いてStoreの値を呼び出してみましょう。

function BearCounter() {
  const bears = useStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}

このように、簡単に呼び出すことができます。

Zustandによる状態管理の基本構成は以上です。reduxと比べてとても簡潔で、描かなければならないコードが少ないことを感じていただけたのではないでしょうか?

スライスパターン

とはいえ、上記の基本構成では管理したいstateが増えると管理が大変になりそうです。そんな時はスライスパターンを使うことが推奨されています。

スライスパターンを採用することで、大きくなってしまったStoreを個別の小さなStoreに分割することができます。

スライスの書き方は以下の通りです。Storeを作った時と少し書き方が異なるだけで、ほぼ同じです。

fishについてのStore

export const createFishSlice = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

bearについてのStore

export const createBearSlice = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})

2つのStoreを合体

import { create } from 'zustand'
import { createBearSlice } from './bearSlice'
import { createFishSlice } from './fishSlice'

export const useBoundStore = create((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}))

stateの呼び出し

import { useBoundStore } from './stores/useBoundStore'

function App() {
  // いずれもuseBoundStoreから呼び出すことができている
  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>
  )
}

export default App

const bears = useBoundStore((state) => state.bears)のように呼び出すことで、無関係なstateが更新されても再レンダリングが起きないように管理されています。

TypeScript対応

ZustandはTypeScriptにも対応しています。

基本構成の場合

import { create } from 'zustand'

interface BearState {
  bears: number
  increase: (by: number) => void
}

const useBearStore = create<BearState>()((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
}))

create関数にジェネリクスでstateの型を渡しています。また、型の後に「()」が追加されています。これはカリー化が行われていることを意味します。

ここでカリー化が行われている理由は、型推論の精度をあげるためだそうです。詳しくは公式ページでの解説を参照ください。

スライスパターンの場合

import { create, StateCreator } from 'zustand'

interface BearSlice {
  bears: number
  addBear: () => void
  eatFish: () => void
}

interface FishSlice {
  fishes: number
  addFish: () => void
}

interface SharedSlice {
  addBoth: () => void
  getBoth: () => void
}

const createBearSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  BearSlice
> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})

const createFishSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  FishSlice
> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

const createSharedSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  SharedSlice
> = (set, get) => ({
  addBoth: () => {
    // すでに定義されているものを使いまわすこともできます
    get().addBear()
    get().addFish()
    // 使いまわさない場合は以下のように書くこともできます
    // set((state) => ({ bears: state.bears + 1, fishes: state.fishes + 1 })
  },
  getBoth: () => get().bears + get().fishes,
})

const useBoundStore = create<BearSlice & FishSlice & SharedSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
  ...createSharedSlice(...a),
}))

createBearSlice, createFishSlice, createSharedSliceの型としてStateCreatorが渡されており、StateCreatorにはジェネリクスで4つのものが渡されています。

  1. state全体の型
    そのスライスが依存している他のスライスの型を渡します。
    例えばcreateBearSliceはbearとfishに関係するstateを扱う必要があるので、BearSlice & FishSliceが渡されています。もしeatFish()がなかったら、FishSliceを渡す必要はありません。

  2. ミドルウェアの配列
    ミドルウェアを使いたい場合はここに配列を渡すことがあります。使わない場合は空の配列を渡します。
    例えばloggerを使いたい場合、[["zustand/devtools", never]]を渡します。

  3. ストアミューテラの配列
    ストアの振る舞いをカスタマイズするためのもの。ストアの作成時に追加の機能やロジックを適用したい場合に配列を渡します。
    例えばdevtoolsを使いたい場合、[["zustand/devtools", never]]を渡します。

  4. stateとアクションの型
    このスライスで作成するstateとアクションの型を渡します。
    例えばcreateBearSliceはbearに関するstateとアクションなので、BearSlice型が渡されています。

ReduxとZustandの特徴比較

最後にreduxとzustandの特徴比較表を置いておきます。参考になれば幸いです。

特徴 Zustand Redux
ボイラープレート 少ない 多い
学習コスト 低い 高い
状態管理の規模 小規模・中規模向き 大規模向き
パフォーマンス 効率的な再レンダリング 最適化が必要な場合がある
ツールサポート 限定的 豊富な公式ツール・DevTools

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?