【React】 Propsの連鎖をContextでふせごう
コンポーネントの構造が深い場合、親から子に何度もpropで値を渡して深くのコンポーネントに値を持っていくことをしました。contextを使用すると、深い階層まで一気に持っていくことができます。
Contextを使う場合がある例
Contextを使う場合は、深いコンポーネントに対して、親のコンポーネントから何度もpropして深くまで渡したいという場合です。例えば、

例えば、
export default function App() { const [user, setUser] = useState({ // ... }); return ( <ComponentA user={user} /> ) }
export default function ComponentA(props) { return ( <ComponentB user={props.user} /> ) }
export default function ComponentB(props) { return ( <ComponentC user={props.user} /> ) }
export default function ComponentC(props) { // props.userを使う処理 // ... return ( // ... ) }
のようなパターンでは、App
からComponentC
に渡すまでに何度もpropで値を投げています。Contextを使用することで、何度もpropをせずとも、一気にComponentC
に渡すことができます。
Contextを使用すると一気にジャンプして渡せるので便利です。しかし、下手にそれを使用することは結合の増加につながるので、注意が必要です。
上の例では、ComponentC
はComponentB
とだけ結合しています。しかし、Context
を使用するとApp
とも結合することになり、コンポーネントの使い回しがしづらくなります。
そのため、Contextの多用はおすすめしません。
Contextを作る
Contextを使用するには、作って渡す必要があります。まずは作りましょう。
createContext
をし、それをJSXに変数と紐づけて埋め込みます。作成したcontextは他から利用するため、export
します。
import { createContext } from 'react'; export const UserContext = createContext();
作成したcontext
は、JSXでContext名.Provider
という名前のタグで囲みます。
export default function App() { const user = { name: "Jhon" }; return ( <> <UserContext.Provider value={user}> <ComponentA /> </UserContext.Provider> </> ); }
複数のコンテキストがある場合はネストします。
return ( <div> <UserContext.Provider value={user}> <HogeContext.Provider value={hoge}> <ComponentA /> </HogeContext.Provider> </UserContext.Provider> </div> );
Contextを使う
Contextを使って値を取得する際は、上でexport
したものをimport
します。
import { useContext } from "react" // 利用したいcontextをimport import { UserContext } from "../App" export default function ComponentC() { // useContextを取得して、対象のcontextの値を取得 const user = useContext(UserContext); return ( <> <p>{user.name}</p> </> ) }
useState
して出てきたセッターをvalue={[name, setName]}
のように渡しても、子コンポーネントからは更新できません。ライフサイクルの仕様によるものです。
子コンポーネントから書き換えたい場合は、素直にpropで渡す必要があります。
注意点
コンポーネントの再レンダが増えすぎないように
ContextをProvideする際に
<UserContext.Provider value={user}> <ComponentA /> </UserContext.Provider>
のように書きますが、この場合、user
の値が変わると、その内側の要素がすべて再レンダリングされます。そのため、思わぬ高負荷に繋がる可能性があります。
実際に、次のような構成でuser
の値を更新すると、UserContext.Provider
内のすべてのコンポーネントの関数が実行されることがわかります。
export default function App() { const [user, setUser] = useState({ name: "Jhon" }); const mySetUser = (e) => { setUser({ ...user, name: e.target.value, }); } return ( <> <p>parent: {user.name}</p> <input type="text" value={user.name} onChange={mySetUser} /> <UserContext.Provider value={user}> <ComponentA /> </UserContext.Provider> </> ); }
export default function ComponentA() { console.log("componentA"); return ( <> <p>ComponentA</p> <ComponentB /> </> ) }
export default function ComponentB() { console.log("componentB"); return ( <> <p>ComponentB</p> <ComponentC /> </> ) }
export default function ComponentC() { const user = useContext(UserContext); console.log("componentC"); return ( <> <p>ComponentC</p> <p>component: {user.name}</p> </> ) }
これを防ぐには、関数をメモ化して、何が変わったら更新すればよいのかを伝えることです。これにはReact.memo
を用います。
import { memo } from "react"; import ComponentB from "./ComponentB" export default memo(() => { console.log("componentA"); return ( <> <p>ComponentA</p> <ComponentB /> </> ) });
ComponentB
も同様にmemo
を使用すると、user
の値を更新した際にComponentC
の関数だけが実行されることがわかります。
オブジェクトを渡す場合
オブジェクトを、Provideするタイミングで作成する場合、そのコンポーネントが再レンダされる度にオブジェクトが作成されます。オブジェクトは普通の値と異なり、更新する度に値が変わった扱いになるため、レンダリングが発生してしまいます。
{/* これを返すコンポーネントが再レンダされると、ComponentAも再レンダされる */} <UserContext.Provider value={{name: "Jhon"}}> <ComponentA /> </UserContext.Provider>
これを回避するには、useState
して同じ値を利用できるようにすることです。
export default function App() { const [user] = useState({ name: "Jhon" }); return ( <> {/* Appが再レンダされても、ここは値が変化していないため問題なし */} <UserContext.Provider value={user}> <ComponentA /> </UserContext.Provider> </> ); }
const [user] = useState(/*...*/)
のように配列の2つ目を省略すれば、そもそも取得しないという動作になります。
今回の例では元が完全に固定の値という使い方となるため、セッターを利用させないようにするために[user]
のようにしています。
まとめ
Contextを使用することで、親コンポーネントから深い階層への子孫コンポーネントへの値の受け渡しが簡単になります。ここで、Contextを通した受け渡しでは値の更新はできないことに注意が必要です。
提供する側と使用する側で結合が発生するため、多用はよくありません。
