時折、親コンポーネントから子コンポーネントへ値を流していく以外の方法でコンポーネント間の値のやり取りをしたい時があります。これは子から親であったり、同階層の子同士であったり色々です。
値のやり取りをする方法はいくらかありますが React の再レンダリングルールを満たさない方法でそれを行った場合、値の変化を検知して自動で画面を書き変える機能が働きません。
React のレンダリングルールを満たさずに値を変化させて子以外のコンポーネントに渡す方法は例えば次です。
import "./styles.css"; import React, { PropsWithChildren } from "react"; const Loader: React.FC<PropsWithChildren<{}>> = (props) => { const [loading, setLoading] = React.useState(true); React.useEffect(() => { const tmp = Math.random(); console.log({ msg: "in loader", tmp }); // ローダーの中でローカルストレージに値をセット localStorage.setItem("tmp", String(tmp)); // セットが終わったらローディング終了 setLoading(false); }, []); if (loading) { return <div>loading</div>; } return props.children; }; const Inner = () => { const [msg, setMsg] = React.useState(""); React.useEffect(() => { // ローディング終了コンポーネントの中でローカルストレージの値を取得して表示 setMsg(localStorage.getItem("tmp") || ""); }, []); return <div>msg: {msg}</div>; }; export default function App() { return ( <Loader> <Inner /> </Loader> ); }
あるコンポーネント中でローカルストレージに値を書き込み、他のコンポーネントでローカルストレージから値を読み取ります。上記例の場合では Loader コンポーネント内でローカルストレージの値を書き込み終わった状態で Inner コンポーネントがマウントされることを期待し、Inner コンポーネントがマウントされた時にローカルストレージの値を読み取りに行くことで値の受け取りをすると期待します。
完全な同期実行であれば React のライフサイクルメソッドにしたがって期待通りに動作しますが、それでもローカルストレージの実装と読み書きの速度に依存するといった懸念点があります。仮に React が”props を渡していない Inner コンポーネントは既に定まっているコンポーネントである”と判断してあらかじめレンダリングをするといった最適化を実装していた場合は致命傷です。 またこの方式で値を頻繁に変化させようとする場合、値を書き換えるたびに何らかの方法で値を受け渡すコンポーネントを再レンダリングする必要があります。
React にはレンダリングを行うためのルールがあり、これを満たせば React が適切に現在の値についてコンポーネントを適宜描画してくれます。このレンダリングルールを満たしつつ、コンポーネント間で値のやり取りを行う方法を紹介します。
React.Component – React#shouldComponentUpdate()
ちなみにレンダリングルールは↑リンクにまとめられており、クラスコンポーネントのこのメソッドや関数コンポーネントの相当するフックを使用することで独自のレンダリングルールを決めることもできます。レンダリングルールは大雑把に言えば”state, props の変化が起きたら再レンダリング”です。
React 的に子以外のコンポーネントに値を受け渡す方法で私的にバグが起きにくく、簡潔に書けるものは 2 つ程あります。
一つ目が親の state を書き換える関数を子の props として渡し、都度その関数を通じて値をやり取りする方法です。
これは少数の近接した(親子関係をそれほど辿らなくとも、あるコンポーネントからあるコンポーネントへ辿り着けるぐらい)コンポーネント間で値を共有する場合に便利です。これは外部に露出しない点、都度 props を記述するのが手間な点が理由です。これを使ったデモとソースコードが次です。
import "./styles.css"; import React from "react"; /** 子コンポーネントA */ const ChildComponentA = (props) => { // 値を書き換える場合、親の setState 相当の関数を props 経由で渡してもらう return ( <button type="button" onClick={() => props.emitVal(Math.random())}> ランダムに値を親コンポーネントに送信 </button> ); }; /** 子コンポーネントB */ const ChildComponentB = (props) => { // 値を受けとる場合、親の state 相当の値を props 経由で渡してもらう return <div>受け取った値: {props.val}</div>; }; /** 親コンポーネント */ const Parent = () => { // 親では複数の子で使われている値を state として管理 // 適切に props に渡す const [val, setVal] = React.useState(); return ( <> <ChildComponentA emitVal={setVal} /> <ChildComponentB val={val} /> </> ); }; export default function App() { return <Parent />; }
二つ目がコンテキストを使う方法です。これは UI テーマ、言語設定、認証情報といった多くのコンポーネント間で共用すべき値で使うのに便利です。コンテキストは都度 props を書く必要がなく記述が手軽ですが、グローバル変数的な機能でであり、呼び出すのは便利ですが、このタイプのものが増えすぎると実質的な管理不能に陥ります。コンテキストを使用する機能を厳選した方が無難です。
コンテクスト – React
これを使ったデモとソースコードが次です。
import "./styles.css"; import React from "react"; /** コンテキストとして扱う状態をあらかじめ定義。この初期値を使うことは余りありませんが、何が入るかわかるようにしておくと便利です */ const ValContext = React.createContext({ val: 0, setVal: (newVal) => {} }); // コンテキストを hook として使うための準備 const useValContext = () => React.useContext(ValContext); /** * コンテキストをくくった子コンポーネント全てにおいて使えるようにするためのコンポーネント */ const ValProvider = (props) => { const [val, setVal] = React.useState(0); // 上の方で定義したコンテキスト ValContext に実際に取り扱う値である state の getter, setter をセット return ( <ValContext.Provider value={{ val, setVal }}> {props.children} </ValContext.Provider> ); }; /** 子コンポーネントA */ const ChildComponentA = () => { // コンテキストから set 用の関数を渡してもらう const { setVal } = useValContext(); return ( <button type="button" onClick={() => setVal(Math.random())}> ランダムに値を親コンポーネントに送信 </button> ); }; /** 子コンポーネントB */ const ChildComponentB = () => { // コンテキストから値を直接渡してもらう const { val } = useValContext(); return <div>受け取った値: {val}</div>; }; /** 親コンポーネント */ const Parent = () => { // コンテキストのproviderをセット return ( <ValProvider> <ChildComponentA /> <ChildComponentB /> </ValProvider> ); }; export default function App() { return <Parent />; }
一応、確実に同期的に動く処理や値の変化を自分で持ちつつ変化を React に伝える、といった方法を実装することでグローバルな素の JavaScript の領域を使っても前述のコードらと同様の動作を行うことは可能です。