Without Redux!
Reactといば切っても切れないのがRedux、Global stateの管理のために長年一緒に使われてきた。 Hooksが実装されてからはかなり様相が変わったものの、未だにReduxを使っているプロジェクトも珍しくない。実際Reduxには便利なDev toolやLibraryがあるので、選択肢として未だにアリだとは思うが、今回はReduxを使わずにHooksとContext APIを使ってGlobal stateを管理する方法について書いていく。
作るもの
この手のチュートリアルでよくある増減可能なカウンターと、入力したテキストがGlobal stateとして更新される仕組みを作ってみる。カウンターもテキストもGlobal stateなので、更新した際に親コンポーネントでも子コンポーネントでも同時に更新されるのが確認できる。 また子コンポーネントからGlobal stateを更新することも可能。
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>
)
}
今回はcountとtextを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()はreducerとinitialStateを受け取り、stateとdispatchを返す。
const [state, dispatch] = useReducer(reducer, initialState)
ここで返されるstateがContextで流すべきGlobal stateになる。 そしてさっき言及したdispatchはこのdispatchのこと。これを使ってアクション(action)を実行するので、これもContextの中に流す。
次にreducer本体。だがその前にactionが通常どういう形になるか理解しておく必要がある。
const action = {
type: 'update_count',
payload: 123
}
このように、typeでactionの種類を指定して、値は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を管理することができた。