本文へスキップ
Context

Part 9 · state を共有する

48 / 64

Context

バケツリレーをやめる

Part 4 で「state はそれを使う部品すべてを含む、いちばん下に置く」 と書きました。

ところが実際のアプリでは、その「いちばん下」がかなり上のほうになることがあります。 ログイン中のユーザー、テーマの設定、言語。 アプリのどこからでも使いたいものです。

すると、間の部品が使いもしない props を受け渡すだけになります。それを解決するのが Context です。

間の部品が、ただの通り道になる

user を使うのは、いちばん下の Profile だけです。 ですが、そこへ届けるために Layout Sidebar も受け取る必要があります。

props で下まで運ぶうまくいかない例

間の 2 つは user を使わないのに受け取っている

Layout(素通しするだけ)
Sidebar(素通しするだけ)
Profile(使う人)
さとう さん

これをバケツリレーと呼びます。 深さが 2 段や 3 段なら大した問題ではありません。 つらくなるのは次のような場合です。

  • 階層が 5 段、6 段と深くなる
  • 運ぶものが増える(user だけでなく theme、language も…)
  • 途中に部品を挟むたびに、全部の中継地点を書き換える必要がある

とくに最後がつらい。SidebarProfile の間にもう 1 つ挟むだけで、 そこにも props を書き足さなければなりません。

prop-drilling.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { useState } from "react";

// いちばん下の部品。ここだけが user を使う
function Profile({ user }: { user: string }) {
  return <RenderBox title="Profile(使う人)">{user} さん</RenderBox>;
}

// 途中の部品。user を使わないのに、渡すためだけに受け取っている
function Sidebar({ user }: { user: string }) {
  return (
    <RenderBox title="Sidebar(素通しするだけ)">
      <Profile user={user} />
    </RenderBox>
  );
}

// ここも同じ。使わないのに受け取って、下に渡す
function Layout({ user }: { user: string }) {
  return (
    <RenderBox title="Layout(素通しするだけ)">
      <Sidebar user={user} />
    </RenderBox>
  );
}

export function PropDrilling() {
  const [user, setUser] = useState("さとう");

  return (
    <div className="flex flex-col gap-4">
      <Button
        size="sm"
        onClick={() => setUser(user === "さとう" ? "すずき" : "さとう")}
      >
        名前を変える
      </Button>

      <Layout user={user} />
    </div>
  );
}

置き場所を作って、直接取りに行く

Context は値の置き場所です。 一度置けば、その内側にいる部品はどこからでも取り出せます。 間の部品を経由しません。

使うのは 3 つだけです。

// 1. 置き場所を作る
const UserContext = createContext<string>("");

// 2. 値を置く(この中にいる部品が対象)
<UserContext value={user}>
  <Layout />
</UserContext>

// 3. 取り出す
const user = use(UserContext);
Context で受け取る直した例

Layout と Sidebar は user を知らない

Layout(何も知らない)
Sidebar(何も知らない)
Profile(使う人)
さとう さん

LayoutSidebar からprops が消えました。 間に何段挟んでも、書き換える必要はありません。

with-context.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, use, useState } from "react";

// 値の置き場所を作る
const UserContext = createContext<string>("");

// 使う側は、この関数を呼ぶだけ
const useUser = () => use(UserContext);

// いちばん下の部品。props を受け取らず、自分で取りに行く
function Profile() {
  const user = useUser();
  return (
    <RenderBox title="Profile(使う人)" tone="highlight">
      {user} さん
    </RenderBox>
  );
}

// 途中の部品は user を知らない。props も受け取らない
function Sidebar() {
  return (
    <RenderBox title="Sidebar(何も知らない)">
      <Profile />
    </RenderBox>
  );
}

function Layout() {
  return (
    <RenderBox title="Layout(何も知らない)">
      <Sidebar />
    </RenderBox>
  );
}

export function WithContext() {
  const [user, setUser] = useState("さとう");

  return (
    <div className="flex flex-col gap-4">
      <Button
        size="sm"
        onClick={() => setUser(user === "さとう" ? "すずき" : "さとう")}
      >
        名前を変える
      </Button>

      {/* この中にいる部品なら、どこからでも user を取り出せる */}
      <UserContext value={user}>
        <Layout />
      </UserContext>
    </div>
  );
}

取り出す部分は関数にまとめる

デモでは、取り出す処理を useUser という関数にしています。

const useUser = () => use(UserContext);

1 行ですが、これには意味があります。

  • 使う側が UserContext という置き場所の存在を知らなくて済む
  • あとで「値がないときはエラーにする」といった処理を、ここ 1 か所に足せる
// あとからこう強化できる
const useUser = () => {
  const user = use(UserContext);
  if (!user) throw new Error("UserContext の中で使ってください");
  return user;
};

Context を作るときは、この取り出し用の関数もセットで用意するのが定番です。

with-context.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, use, useState } from "react";

// 値の置き場所を作る
const UserContext = createContext<string>("");

// 使う側は、この関数を呼ぶだけ
const useUser = () => use(UserContext);

// いちばん下の部品。props を受け取らず、自分で取りに行く
function Profile() {
  const user = useUser();
  return (
    <RenderBox title="Profile(使う人)" tone="highlight">
      {user} さん
    </RenderBox>
  );
}

// 途中の部品は user を知らない。props も受け取らない
function Sidebar() {
  return (
    <RenderBox title="Sidebar(何も知らない)">
      <Profile />
    </RenderBox>
  );
}

function Layout() {
  return (
    <RenderBox title="Layout(何も知らない)">
      <Sidebar />
    </RenderBox>
  );
}

export function WithContext() {
  const [user, setUser] = useState("さとう");

  return (
    <div className="flex flex-col gap-4">
      <Button
        size="sm"
        onClick={() => setUser(user === "さとう" ? "すずき" : "さとう")}
      >
        名前を変える
      </Button>

      {/* この中にいる部品なら、どこからでも user を取り出せる */}
      <UserContext value={user}>
        <Layout />
      </UserContext>
    </div>
  );
}

ただし、すぐには使わない

Context を使うと、代わりに失うものがあります。

  • どこから値が来ているか、コードを見ても分からなくなる。 props なら渡している場所をたどれます
  • その部品はContext の中でしか動かなくなる。 単体で使い回せなくなります
  • 次の章でやるとおり、再レンダリングの範囲が広がりやすい

Part 2 でやった合成で解決できることも多くあります。 中身を親で組み立てて children として渡せば、 そもそも中継が発生しません。

with-context.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, use, useState } from "react";

// 値の置き場所を作る
const UserContext = createContext<string>("");

// 使う側は、この関数を呼ぶだけ
const useUser = () => use(UserContext);

// いちばん下の部品。props を受け取らず、自分で取りに行く
function Profile() {
  const user = useUser();
  return (
    <RenderBox title="Profile(使う人)" tone="highlight">
      {user} さん
    </RenderBox>
  );
}

// 途中の部品は user を知らない。props も受け取らない
function Sidebar() {
  return (
    <RenderBox title="Sidebar(何も知らない)">
      <Profile />
    </RenderBox>
  );
}

function Layout() {
  return (
    <RenderBox title="Layout(何も知らない)">
      <Sidebar />
    </RenderBox>
  );
}

export function WithContext() {
  const [user, setUser] = useState("さとう");

  return (
    <div className="flex flex-col gap-4">
      <Button
        size="sm"
        onClick={() => setUser(user === "さとう" ? "すずき" : "さとう")}
      >
        名前を変える
      </Button>

      {/* この中にいる部品なら、どこからでも user を取り出せる */}
      <UserContext value={user}>
        <Layout />
      </UserContext>
    </div>
  );
}

理解できたか確かめる

確認クイズ

Context を使うと何が解決する?

確認クイズ

バケツリレーが 3 段ある。Context を使うべき?

with-context.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, use, useState } from "react";

// 値の置き場所を作る
const UserContext = createContext<string>("");

// 使う側は、この関数を呼ぶだけ
const useUser = () => use(UserContext);

// いちばん下の部品。props を受け取らず、自分で取りに行く
function Profile() {
  const user = useUser();
  return (
    <RenderBox title="Profile(使う人)" tone="highlight">
      {user} さん
    </RenderBox>
  );
}

// 途中の部品は user を知らない。props も受け取らない
function Sidebar() {
  return (
    <RenderBox title="Sidebar(何も知らない)">
      <Profile />
    </RenderBox>
  );
}

function Layout() {
  return (
    <RenderBox title="Layout(何も知らない)">
      <Sidebar />
    </RenderBox>
  );
}

export function WithContext() {
  const [user, setUser] = useState("さとう");

  return (
    <div className="flex flex-col gap-4">
      <Button
        size="sm"
        onClick={() => setUser(user === "さとう" ? "すずき" : "さとう")}
      >
        名前を変える
      </Button>

      {/* この中にいる部品なら、どこからでも user を取り出せる */}
      <UserContext value={user}>
        <Layout />
      </UserContext>
    </div>
  );
}

この章のまとめ

  • Context は値の置き場所。内側の部品がどこからでも取り出せる
  • 使うのは 3 つ。createContext / <Context value=...> / use(Context)
  • 取り出す関数をセットで用意しておくと、あとで強化できる
  • 最後の手段。まずリフトアップと合成を試す。使うならアプリ全体で共有するものに限る

with-context.tsx

"use client";

import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { createContext, use, useState } from "react";

// 値の置き場所を作る
const UserContext = createContext<string>("");

// 使う側は、この関数を呼ぶだけ
const useUser = () => use(UserContext);

// いちばん下の部品。props を受け取らず、自分で取りに行く
function Profile() {
  const user = useUser();
  return (
    <RenderBox title="Profile(使う人)" tone="highlight">
      {user} さん
    </RenderBox>
  );
}

// 途中の部品は user を知らない。props も受け取らない
function Sidebar() {
  return (
    <RenderBox title="Sidebar(何も知らない)">
      <Profile />
    </RenderBox>
  );
}

function Layout() {
  return (
    <RenderBox title="Layout(何も知らない)">
      <Sidebar />
    </RenderBox>
  );
}

export function WithContext() {
  const [user, setUser] = useState("さとう");

  return (
    <div className="flex flex-col gap-4">
      <Button
        size="sm"
        onClick={() => setUser(user === "さとう" ? "すずき" : "さとう")}
      >
        名前を変える
      </Button>

      {/* この中にいる部品なら、どこからでも user を取り出せる */}
      <UserContext value={user}>
        <Layout />
      </UserContext>
    </div>
  );
}