【脱Redux】React hooks だけでグローバルな状態を管理する

前に書いた記事 Context と Hooks でグローバルな状態を管理する一番シンプルな方法 - 藤 遥のブログ を発展させた。

やりたいことは以下。

  • Redux を使わずに React だけでグローバルな状態を管理したい
  • 状態は関心事に空間分けしたい
  • TypeScript を使うが型定義の記述量は少なく済ませたい

で、最小のサンプルコードを書いた。

まずは状態。これは普通の Hook。

import { useState } from 'react'

export default function useCounterState() {
  const [count, setCount] = useState(0)
  return {
    count,
    setCount,
  }
}

export type CounterState = ReturnType<typeof useCounterState>

工夫したのは、型定義の記述を減らすために ReturnType<T> を使ったところ。状態はアプリケーションが大きくなるとファイル数がめっちゃ増えていくイメージ。ただ、一つ一つの状態はこんなふうに小さなファイルになる。

次は、すべての状態を統合して単一の Store として提供する箇所。1つの Context にまとめる。

import React, { createContext, ReactChild, useContext } from 'react'
import useCounterState, { CounterState } from './useCounterState'

type StoreState = {
  counter: CounterState
  counter2: CounterState
}

const StoreContext = createContext<StoreState>(null as any)

const useStore = () => useContext(StoreContext)
export const StoreProvider = (props: { children: ReactChild }) => {
  const value = {
    counter: useCounterState(),
    counter2: useCounterState(),
  }
  return (
    <StoreContext.Provider value={value}>
      {props.children}
    </StoreContext.Provider>
  )
}
export default useStore

この例では CounterState を2回使っている。export するものは2つあって、useStore は Store から値を受け取るコンポーネントが使用する。StoreProvider は Context の Provider なので、コンポーネントのトップに置く。

そしてコンポーネント側。

import React from 'react'
import ReactDOM from 'react-dom'
import useStore, { StoreProvider } from './useStore'

const Counter = () => {
  const store = useStore()
  return (
    <div onClick={() => store.counter.setCount((count) => count + 1)}>
      {store.counter.count}
    </div>
  )
}

const App = () => {
  return (
    <>
      <Counter />
    </>
  )
}

ReactDOM.render(
  <StoreProvider>
    <App />
  </StoreProvider>,
  document.getElementById('root'),
)

useStore() を使えばいつでもグローバルな状態にアクセスできる。TypeScript なら強力なエディタ補完のサポートを受けられるので、store がネストしても間違えることがない。

パフォーマンス上の注意点として、store が更新されると useStore() が使われるコンポーネントすべてが再 render されるので、むやみにたくさん useStore() すると遅くなるかもしれない。まあその点は Redux の connect() も同じだった気もするけど、どうだったか忘れた。

パフォーマンスに気をつけるためには、Hooks で提供されている useMemo() とか useCallback() とかを効果的に使うとよさそう。

あとこの方法の欠点は、store のどの状態がどのコンポーネントで使われているのか探しにくい。状態ごと(つまり counter とかの単位で)Context を作ることにすれば、ファイルの依存関係から Context を使っているコンポーネントを探せるけど、そうすると記述量が増えるからやりたくない…。まあこの問題はいったん措く。