はじめに
React初級者ぐらいの私が、Functional ComponentでHooks APIを使って以下をどう書くか知らべた結果を書きます。
- 親と孫でstateを共通管理する
- 親から孫の関数を呼び出す
- 子は、上記に関することを何も書かない
- React-Redux ってどうなる?
以下のHooks APIを使うことで実現できました。
- useState
- useContext
- useReducer
成果物
公式にもよく出てくる、Counterを+や-ボタンで増減させるアプリです。
ここでコード参照できます。
背景
個々のhooksの機能や1ファイルでの書き方しか見つからず、親子孫がファイル分割された状態でどう書くの?というのが分からなかったので、調べながら実際に実装してみました。
解説
ファイル構成
public/components/
以下がメインで、このような構成になっています。
parent.js・・・親component
child.js・・・子component
grand_child.js・・・孫component
(一番知りたかったのは上記3つをどう書くか)
context.js・・・コンテキスト処理の共通化
reducer.js・・・reducer処理
この構成で、親と孫間で、共通のstate参照や、関数を呼び出したいときに
parent.js
, grand_child.js
に何を書くのか、というのが知りたかったポイントです。
コードの解説
親Component
まずは、関数(コールバック)やstateを共有したいcomponent達の親となるcomponent parent.js
に、useReducerを書きます。
export default function ParentComponent() {
const [state, dispatch] = useReducer(reducer, initialState)
※ reducer
と initialState
はreducer.jsに切り出しました。
state
やdispatch
を親component配下のcomponent達で参照できる用contextを用意します。
import {ContextAppDispatch, ContextState} from "./context.js"
context.js は、孫でも参照するので外部化しました。
import React from "react"
export const ContextAppDispatch = React.createContext("AppDispatch");
export const ContextState = React.createContext("state");
定義したものを、配下のcomponentが受け取れるようにします。
return (
<ContextAppDispatch.Provider value={dispatch}>
<ContextState.Provider value={state}>
<h1>ParentComponent</h1>
<p>Count: {state.count}</p>
(中略)
<ChiledComponent />
</ContextState.Provider>
</ContextAppDispatch.Provider>
Contextが2重になっているのは後述します。
孫Componentで呼び出し
useContextで呼び出します。
import {useContext} from "react"
import {ContextAppDispatch,ContextState} from "./context.js"
export default function GrandChildComponent() {
const dispatch = useContext(ContextAppDispatch);
const state = useContext(ContextState);
return (
<>
<h1>GrandChildComponent</h1>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
こうすることで、以下ができました。
- 親で定義したdispatchを孫も呼べる
- 親で定義したstateを孫も参照できる
このとき、子は何も上記に関与していません。
import GrandChiledComponent from './grand_child.js'
export default function ChildComponent() {
return (
<>
<h1>ChildComponent</h1>
<GrandChiledComponent />
</>
)
}
複数のcomponent間でのデータやコールバックの受け渡し方法
今回の実装方針として、公式に書かれている方法にできるだけ沿うようにしました。
公式に以下の記載があります.
大きなコンポーネントツリーにおいて我々がお勧めする代替手段は、useReducer で dispatch 関数を作って、それをコンテクスト経由で下の階層に渡す
(中略)
アプリケーションの state については、props として渡していくか(より明示的)、あるいはコンテクスト経由で渡すか(深い更新ではより便利)を選ぶ余地が依然あります。
ということで、(今回は大きなコンポーネントツリーではないですが)コールバックもstateも受け渡しはcontextでまとめることで、コードはすっきりしたと思います。
複数のcontextを使う
そうすると、
- dispatch用context
- state用context
の2つが必要になります。1つのcontextでも実装可能ですが、[公式](コンテクストの再レンダーを高速に保つために、React は各コンテクストのコンシューマをツリー内の別々のノードにする必要があります。)には
コンテクストの再レンダーを高速に保つために、React は各コンテクストのコンシューマをツリー内の別々のノードにする必要があります。
とあるので、分けました。
それが以下の部分
export const ContextAppDispatch = React.createContext("AppDispatch");
export const ContextState = React.createContext("state");
<ContextAppDispatch.Provider value={dispatch}>
<ContextState.Provider value={state}>
(中略)
</ContextState.Provider>
</ContextAppDispatch.Provider>
できたことまとめ
- 親、子、孫をFunctional Componentで実装できた
- 子に何も書かず、親と孫で共通のstateやactionを呼び出せた
疑問・課題
やり残したことや、新たに出てきた疑問など。React詳しい方のツッコミもらえると幸いです。
- contextは、 context.js を外部化して共通利用という方法で合ってる?
- reducer.js とか context.js の保存場所は componentsディレクトリとは別のディレクトリが良さそう
- action部分
{type: 'increment'}
も外部化したほうが良さそう