【脱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 を使っているコンポーネントを探せるけど、そうすると記述量が増えるからやりたくない…。まあこの問題はいったん措く。