こんにちは、フロントエンドエンジニアの松尾です。
Reactにはフック(Hooks)と呼ばれる機能があり、useState
・useEffect
・useCallback
といった様々な関数が用意されています。
その中の一つにuseReducer
がありますが、useContext
と併用して使うものとして認識されたり、useState
の影に隠れたりといった理由であまり活用されていない傾向があります。
しかし、useReducer
は様々な場面で活用でき、可読性・パフォーマンス向上に大いに役立ちます。
本記事では、useReducer
の基本的な使い方と様々な活用法について説明したいと思います。
useReducerとは?
useReducer
とは、useState
と同様に状態管理ができるフックです。
特に複雑なロジックが絡んだ状態を管理するのに適しています。
useReducer
は以下のように宣言することで使用できます。
const [state, dispatch] = useReducer(reducer, initialState)
必要な引数は、reducer
とinitialState
です。
reducer:stateとactionを受け取り、新しいstateを返す関数
initialState:stateの初期値
戻り値としてstate
とdispatch
を返します。
state : 現在のstate値
dispatch : reducerを実行するための関数
では実際に、useReducer
の使用例を見てみましょう。以下はReact公式ドキュメントから引用したコードになります。
const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }
initialState
でカウント値の初期値を定義しており、reducerにてincrement
とdecrement
のアクションを用意しています。
処理の流れは以下の通りです。
ボタンを押すとdispatchが実行され、中身がreducerのactionに入る。
switch文によって処理を分岐させ、新しいstate値を計算して返す。
useReducerが更新されたstate値を受け取り、新しいstate値を描画させる。
reduxの書き方と非常に似ていますが、useReducerはdispatchにオブジェクトを返す必要はなく、値をそのまま入れることも可能です。
const initialState = {count: 0}; function reducer(state, action) { return state + action; } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch(-1)}>-</button> <button onClick={() => dispatch(+1)}>+</button> </> ); }
このようにシンプルに記述できることもuseReducer
の魅力の一つです。
ここまでの説明で「useState
でもよくない?」と思われた方も多いのではないでしょうか?
ここからは、useReducer
ならではの強みについて説明したいと思います。
状態管理をuseReducerにまとめられる
useState
で状態管理していると、宣言が多すぎて複雑になってしまうことはありませんか?
例えば以下のような場合です。
const App = () => { const [isOpen, setIsOpen] = useState(false) const [type, setType] = useState('small') const [phone, setPhone] = useState('') const [email, setEmail] = useState('') const [error, setError] = useSatte(null) return ( ... ) }
このように状態管理が多い場合、ロジックが複雑になるにつれ可読性が悪化してしまうケースが多々あります。
状態管理をuseReducer
にまとめることで「何を状態管理しているか」「初期値はなんなのか」が明確になります。
const initialState = { isOpen: false, type: 'small', phone: '', email: '', error: null } const reducer = (state, action) => { switch (action.type) { ... default: return state } } const App = () => { const [state, dispatch] = useReducer(reducer, initialState) return ( ... ) }
ロジックとビューを切り離すことができる
useReducer
のもう一つの強みは、ロジックとビューを切り離すことができる点です。
今回サンプルとして、以下を実装します。
3つのitemがあり、それぞれの値の更新・削除ができるといったシンプルな実装です。
上記をuseState
とuseReducer
それぞれで実装し見比べてみましょう。
まずuseState
の場合です。
const App = () => { const [items, setItems] = useState([0, 0, 0]); const increment = (index) => { const newItems = items.slice() newItems[index]++ setItems(newItems) } const decrement = (index) => { const newItems = items.slice() newItems[index]-- setItems(newItems) } const deleteItem = (index) => { setItems(items.filter((_, i) => i !== index)) } return ( <> { items.map((v, i) => ( <div key={i}> item{i}: {v} <button onClick={() => increment(i)}>+</button> <button onClick={() => decrement(i)}>-</button> <button onClick={() => deleteItem(i)}>削除</button> </div> )) } </> ); }
このように、useState
の場合は、ロジックとビューが混ざった状態となっています。
この程度の規模であれば問題ないかと思いますが、もしロジックが複雑になってきた場合、どの関数がどの状態を更新しているのかが分かりづらくなってしまいます。
次に、useReducer
の場合です。
const myReducer = (state, action) => { const newState = state.slice() switch (action.type) { case 'increment': newState[action.index]++ return newState case 'decrement': newState[action.index]-- return newState case 'delete': return state.filter((_, i) => i !== action.index); default: throw new Error(); } }; const App = () => { const [items, dispatch] = useReducer(myReducer, [0, 0, 0]); return ( <> { items.map((v, i) => ( <div key={i}> item{i}: {v} <button onClick={() => dispatch({type: 'increment', index: i})}> + </button> <button onClick={() => dispatch({type: 'decrement', index: i})}> - </button> <button onClick={() => dispatch({type: 'delete', index: i})}> 削除 </button> </div> )) } </> ); }
このように、useReducer
内にロジック処理を記述できるため、ビューと完全に切り離すことができます。
もし機能追加となった場合も、わざわざ関数を用意せずに、reducerのswitch文に新しく処理を追加するだけで済みます。
また、useReducer
を用いるとdispatch関数はメモ化されるため、もし返ってくる値が変わらない場合はコンポーネントの再レンダリングを防ぐこともできます。
以上のことから、ロジックが複雑な処理に関しては、なるべくuseReducer
を使用することをおすすめします。
まとめ
以上useReducer
の基本的な使い方と活用法についての説明でした。
useReducer
は、useState
と比べると使い方が馴染みづらく避けがちですが、活用することで様々な処理の可読性・パフォーマンス向上に役立ちます。
是非今回紹介した方法をお試し下さい!
最後になりますが、Wizではエンジニアを募集中です。
興味のある方は是非覗いてみてください↓