【React】 Propsの連鎖をContextでふせごう

コンポーネントの構造が深い場合、親から子に何度もpropで値を渡して深くのコンポーネントに値を持っていくことをしました。contextを使用すると、深い階層まで一気に持っていくことができます。

Contextを使う場合がある例

Contextを使う場合は、深いコンポーネントに対して、親のコンポーネントから何度もpropして深くまで渡したいという場合です。例えば、

propsとcontextの値の渡し方の違い

例えば、

App.jsx
export default function App() {
    const [user, setUser] = useState({
        // ...
    });

    return (
        <ComponentA user={user} />
    )
}
ComponentA.jsx
export default function ComponentA(props) {
    return (
        <ComponentB user={props.user} />
    )
}
ComponentB.jsx
export default function ComponentB(props) {
    return (
        <ComponentC user={props.user} />
    )
}
ComponentC.jsx
export default function ComponentC(props) {
    // props.userを使う処理
    // ...

    return (
        // ...
    )
}

のようなパターンでは、AppからComponentCに渡すまでに何度もpropで値を投げています。Contextを使用することで、何度もpropをせずとも、一気にComponentCに渡すことができます。

Contextを使用すると一気にジャンプして渡せるので便利です。しかし、下手にそれを使用することは結合の増加につながるので、注意が必要です。

上の例では、ComponentCComponentBとだけ結合しています。しかし、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します。

components/ComponentC.jsx
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>
        </>
    );
}
ComponentA
export default function ComponentA() {
    console.log("componentA");

    return (
        <>
        <p>ComponentA</p>
        <ComponentB />
        </>
    )
}
ComponentB
export default function ComponentB() {
    console.log("componentB");

    return (
        <>
        <p>ComponentB</p>
        <ComponentC />
        </>
    )
}
ComponentC
export default function ComponentC() {
    const user = useContext(UserContext);
    console.log("componentC");

    return (
        <>
        <p>ComponentC</p>
        <p>component: {user.name}</p>
        </>
    )
}

これを防ぐには、関数をメモ化して、何が変わったら更新すればよいのかを伝えることです。これにはReact.memoを用います。

ComponentA
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を通した受け渡しでは値の更新はできないことに注意が必要です。

提供する側と使用する側で結合が発生するため、多用はよくありません。

react context thumb

役に立ったらシェアしよう!