ReactでuseStateではなくuseReducerを使う方法

useReducerとは

useReducerはuseStateのように現在の状態(state)を保持します。

useStateとの違いは、状態を更新するためにアクションを送信(ディスパッチ)するdispatch関数と、現在の状態とアクションを受け取り、新しい状態を返すreducer関数を利用することです。

使用する際はuseStateのように 'react' からuseReducerをインポートして、以下のような形で使用します。

TSX
import { useReducer } from 'react'

const [state, dispatch] = useReducer(reducer, initialState)

例えば「+」「-」ボタンを押して1ずつ増減するカウンターの場合は以下のようなコードになります。

App.tsx
import { useReducer } from 'react'

type Action = {
  type: 'increment' | 'decrement'
}

type State = {
  count: number
}

function reducer(state: State, action: Action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      return state
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  )
}

export default App

これだけだとuseStateとあまり変わりません。useReducerはreducerの関数をreducer.tsのような別ファイルに書き出せば、ほかのコンポーネントでも利用できるというメリットがあります。

例えば以下のようにreducer.tsにreduce関数のコードを移してインポートして、別途ChildCounterというコンポーネントを作成して、そこでもreduce関数をインポートすれば、同じ機能を使用できます。

App.tsx
import { useReducer } from 'react'
import { reducer } from './reducer'
import ChildCounter from './ChildCounter'

function App() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <hr />
      <ChildCounter />
    </div>
  )
}

export default App
reducer.ts
type Action = {
  type: 'increment' | 'decrement'
}

type State = {
  count: number
}

export function reducer(state: State, action: Action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      return state
  }
}
ChildCounter.tsx
import { useReducer } from 'react'
import { reducer } from './reducer'

function ChildCounter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  )
}

export default ChildCounter

reducer関数を親と子でインポートして利用するサンプル

もし、子コンポーネントの増減ボタンで親の{state.count}を変更する場合は、コンポーネントにdispatchを渡して、propsで受け取って利用します。

App.tsx
import { useReducer } from 'react'
import { reducer } from './reducer'
import ChildCounter from './ChildCounter'

function App() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <hr />
      <ChildCounter dispatch={dispatch} />
    </div>
  )
}

export default App
reducer.ts
type Action = {
  type: 'increment' | 'decrement'
}

type State = {
  count: number
}

export function reducer(state: State, action: Action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      return state
  }
}

export type DispatchType = React.Dispatch<Action>
ChildCounter.tsx
import { DispatchType } from './reducer'

type Props = {
  dispatch: DispatchType
}

function ChildCounter({ dispatch }: Props) {
  return (
    <div>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  )
}

export default ChildCounter

初心者の方だとuseStateしか知らない方が多いですが、複数コンポーネントで同じ機能を使用したいときなどはuseReducerが必要になるので、覚えておくと良いです。