Part 9 · state を共有する
48 / 64 章
Context
バケツリレーをやめる
Part 4 で「state はそれを使う部品すべてを含む、いちばん下に置く」 と書きました。
ところが実際のアプリでは、その「いちばん下」がかなり上のほうになることがあります。 ログイン中のユーザー、テーマの設定、言語。 アプリのどこからでも使いたいものです。
すると、間の部品が使いもしない props を受け渡すだけになります。それを解決するのが Context です。
間の部品が、ただの通り道になる
user を使うのは、いちばん下の Profile だけです。 ですが、そこへ届けるために Layout と Sidebar も受け取る必要があります。
間の 2 つは user を使わないのに受け取っている
これをバケツリレーと呼びます。 深さが 2 段や 3 段なら大した問題ではありません。 つらくなるのは次のような場合です。
- 階層が 5 段、6 段と深くなる
- 運ぶものが増える(user だけでなく theme、language も…)
- 途中に部品を挟むたびに、全部の中継地点を書き換える必要がある
とくに最後がつらい。Sidebar と Profile の間にもう 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);Layout と Sidebar は user を知らない
Layout と Sidebar から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>
);
}