[React] HooksとContext APIでGlobal stateを管理する

November 12, 2021

programming
article cover

Without Redux!

Reactといば切っても切れないのがRedux、Global stateの管理のために長年一緒に使われてきた。 Hooksが実装されてからはかなり様相が変わったものの、未だにReduxを使っているプロジェクトも珍しくない。実際Reduxには便利なDev toolやLibraryがあるので、選択肢として未だにアリだとは思うが、今回はReduxを使わずにHooksとContext APIを使ってGlobal stateを管理する方法について書いていく。

作るもの

この手のチュートリアルでよくある増減可能なカウンターと、入力したテキストがGlobal stateとして更新される仕組みを作ってみる。カウンターもテキストもGlobal stateなので、更新した際に親コンポーネントでも子コンポーネントでも同時に更新されるのが確認できる。 また子コンポーネントからGlobal stateを更新することも可能。

screen captcha

Contextを作る

まずはGlobal stateを作り、それをContext.Providerを使ってアプリ全体に流しこむ。そうすることでアプリの好きな場所でGlobal stateにアクセスすることができる。

// store.js

import React, { createContext } from 'react'

const initialState = {
  count: 0,
  text: '',
}

export const Context = createContext(initialState) // <- initialStateを使ってContextを作成

export const Store = ({ children }) => {
  return (
    <Context.Provider value={[initialState]}> {/* <- valueに渡したものがGlobal stateに */} 
      {children}
    </Context.Provider>
  )
}

今回はcounttextをGlobal stateとして設定する。
createContext()でinitialStateを元にContextを作り、<Context.Provider>でアプリを包む。このときvalue={}に入れたものがアプリ全体でアクセスできるものになる。
propsとしてchildrenを受け取り、renderするのを忘れないように。

// index.js

import React from 'react'
import ReactDOM from 'react-dom'

import App from './App'
import { Store } from './store'

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

Global stateを使うときはuseContextを使う。先ほど作ったContextを引数として渡すことでvalue={}に入れたstateを取り出すことができる。

// somewhere in your project 

import React, { useContext } from 'react'
import { Context } from './store'

export const Comp = () => {
  const [state] = useContext(Context) // <- ContextからGlobal stateを取り出す
  
  return <div>{state.count}</div>
}

Reducerを作る

Contextを作って好きな場所でGlobal stateにアクセスできるようになったが、このままだとGlobal stateを変更することができない。
そこでReducerを作る。Reducerの役割はdispatchされたアクション(action)を受け取って、新しいstateを返すこと。つまりアクションの内容で変更内容や値を判別してstateを更新する。

// store.js

import React, { createContext, useReducer } from 'react';

const initialState = {
  count: 0,
  text: '',
};

export const Context = createContext(initialState);

export const Store = ({ children }) => {
  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'increment':
        return { ...state, count: state.count + 1 }
      
      case 'decrement':
        return { ...state, count: state.count - 1 }
      
      case 'update_text':
        return { ...state, text: action.payload }
      
      default:
        return state
    }
  }, initialState);

  return (
    <Context.Provider value={[state, dispatch]}>
      {children}
    </Context.Provider>
  )
}

まずuseReducer()がやっていることから説明していく。
useReducer()reducerinitialStateを受け取り、statedispatchを返す。

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

ここで返されるstateContextで流すべきGlobal stateになる。 そしてさっき言及したdispatchはこのdispatchのこと。これを使ってアクション(action)を実行するので、これもContextの中に流す。

次にreducer本体。だがその前にactionが通常どういう形になるか理解しておく必要がある。

const action = {
  type: 'update_count',
  payload: 123
}

このように、typeactionの種類を指定して、値はpayloadとして渡す。 これを踏まえた上でreducerを見ていく。

const reducer = (state, action) => {
  switch (action.type) {
    case 'update_count':
      return { ...state, count: action.payload }
    
    default:  
      return state
  }
}

reducerは現在のstateと実行されたactionを引数にとる。switch()を使ってaction.typeごとに処理を分け、新しいstateを(必要に応じてpayloadを加えて)返していく。 どのcaseにもかからなかった時にstateが空になってしまうので、default:でstateを返しておく。

使い方

初見だとdispatchでアクションを実行させるのがわかりにくいかもしれないが、stateの時と同じで、useContext(Context)からstateと一緒にdispatchを取り出して、任意の場所とタイミングで実行すればいい。 多分コードを見た方が早い。

// app.js

import React, { useContext, useState } from 'react'
import './style.css'
import { Context } from './store'
import { ChildComponent } from './child'

const App = () => {
  const [text, setText] = useState('')
  const [state, dispatch] = useContext(Context)

  const updateText = () => {
    dispatch({ type: 'update_text', payload: text })
    setText('')
  }

  return (
    <div>
      <h1>Global State without Redux!</h1>
      
      <section>
        <p>You can increment/decrement count</p>
        <p>count: {state.count}</p>
        <button onClick={() => dispatch({ type: 'increment' })}>+</button>
        <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      </section>
      
      <section>
        <p>text: {state.text}</p>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        <button onClick={updateText}>update text</button>
      </section>

      <ChildComponent />
    </div>
  )
}

export default App

これで無事Reduxを使わずにGlobal stateを管理することができた。

screen captcha

Code


Profile picture

元カメラマン。今はベルリンで働くWEBエンジニア。
スロバキア人の妻とドイツ猫二匹の多国籍家族。