Skip to main content

第76章:useContext の型チェック

null をこわがらなくていいカスタムフックを作ろう 〜


1️⃣ この章でやりたいこと ✨

この章のゴールはたったひとつです👇

useContextnull 問題を、カスタムフックでまるごと隠す」

  • createContext<UserContextValue | null>(null) みたいに null を許す形で Context を作った
  • そのせいで useContext(UserContext) の型が UserContextValue | null になってしまう
  • どこでも毎回 if (!userContext) { ... } とか書くのダルい…

という状況を、カスタムフックで一発解決します 😎


2️⃣ いまの前提コードを整理しよう 📚

ここでは例として「ユーザー名」を共有する Context を使います。 (71〜75章までで作ったものを、ちょっとだけ思い出す感じです)

例として src/contexts/UserContext.tsx を想定します。

今までの書き方(おさらい)

// src/contexts/UserContext.tsx
import { createContext, useState } from "react";

type UserContextValue = {
userName: string;
setUserName: (name: string) => void;
};

// 👇 「まだ値がないよ〜」という意味で null を入れておく
export const UserContext = createContext<UserContextValue | null>(null);

export function UserProvider({ children }: { children: React.ReactNode }) {
const [userName, setUserName] = useState("ゲスト");

const value: UserContextValue = {
userName,
setUserName,
};

return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

ポイント

  • 型は UserContextValue | null
  • createContext の初期値は null
  • UserProvider がいるときだけ、本物の値が入る

この「| null」があるせいで、 useContext(UserContext) を使う側では 毎回 null チェックが必要になります。(LogRocket Blog)


3️⃣ useContext をそのまま使うと何がつらい?😢

たとえば、ユーザー名を表示するコンポーネント。

// src/components/UserNameLabel.tsx
import { useContext } from "react";
import { UserContext } from "../contexts/UserContext";

export function UserNameLabel() {
const userContext = useContext(UserContext);

if (!userContext) {
// Provider で囲まれていなかった場合の保険
return <p>コンテキストが使えません 😭</p>;
}

return <p>こんにちは、{userContext.userName} さん 🌸</p>;
}

問題点

  • 毎回 if (!userContext) { ... } を書くのがめんどくさい
  • うっかり書き忘れると、userContextnull のまま使われてバグる
  • JSXの中も userContext?.userName だらけになりがち…

これを1か所でチェックして、他では気にしなくていいようにしたい、 というのがこの章のテーマです 💪


4️⃣ カスタムフック useUserContext を作る 🎣

やりたいこと

  • useContext(UserContext)ラップしたカスタムフックを作る
  • 中で一度だけ null チェックする
  • null のときは はっきりしたエラーを投げる
  • それ以外のコンポーネント側では const { userName } = useUserContext(); と、安全に使えるようにする

このパターンは TypeScript + React ではかなりよく使われる「定番」になっています。(GitHub)


5️⃣ 実装:UserContext に安全ベルトをつける 🛟

同じファイル UserContext.tsx に、カスタムフックを足します。

// src/contexts/UserContext.tsx
import { createContext, useContext, useState } from "react";

type UserContextValue = {
userName: string;
setUserName: (name: string) => void;
};

export const UserContext = createContext<UserContextValue | null>(null);

export function UserProvider({ children }: { children: React.ReactNode }) {
const [userName, setUserName] = useState("ゲスト");

const value: UserContextValue = {
userName,
setUserName,
};

return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

// ⭐ この章の主役:安全なカスタムフック
export function useUserContext(): UserContextValue {
const context = useContext(UserContext);

if (context === null) {
// React DevTools のコンソールに出るメッセージ
throw new Error("useUserContext は UserProvider の中で使ってください。");
}

// ここに来るときは context は絶対に null じゃない!
return context;
}

ここがポイント!

  • function useUserContext(): UserContextValue { ... }

    • 戻り値の型に | null がなくなるのが大事 🎯
  • 中で useContext(UserContext) を呼んで…

    • if (context === null) { throw new Error(...) }
  • null だったら「ちゃんと Provider で囲んでね!」とエラーを出す

  • null じゃなければ、そのまま返す → 以降は UserContextValue として安心して使える


6️⃣ 使う側のコードがこう変わる ✨

さっきの UserNameLabel を、カスタムフック版に書き換えてみます。

// src/components/UserNameLabel.tsx
import { useUserContext } from "../contexts/UserContext";

export function UserNameLabel() {
// ⭐ もう null チェック不要!
const { userName } = useUserContext();

return <p>こんにちは、{userName} さん 🌸</p>;
}

スッキリしたの、分かりますか?😊

  • if (!userContext)消える

  • JSXも {userName} と素直に書ける

  • もし Provider で囲み忘れていたら?

    • 画面はエラーになってくれて、
    • コンソールに 「useUserContext は UserProvider の中で使ってください」 と教えてくれます

「気づかないままヘンな表示になる」より、 バキッとエラーが出てくれた方がデバッグしやすいんです 🔍


7️⃣ 図でイメージをつかもう 🧠

UserProvider(親)と、UserNameLabel(子)、その間に useUserContext が入っているイメージを Mermaid で描いてみます。

  • UserProviderContext の値を用意して配る
  • 子コンポーネントは直接 useContext ではなく useUserContext にだけ頼む
  • useUserContext の中で nullチェックしてから 値を返す

という流れになっています 🎵


8️⃣ 「外で使ったらどうなるの?」実験 💣

ちょっとわざとミスしてみましょう。

App.tsx をこんなふうに書いたとします(わざとミスです)。

// src/App.tsx
import { UserNameLabel } from "./components/UserNameLabel";
// ❌ UserProvider で囲み忘れてる!

function App() {
return (
<div>
<h1>テストアプリ</h1>
<UserNameLabel />
</div>
);
}

export default App;

この状態で画面を開くと…

  • 画面がエラーになって止まる
  • コンソールに 「useUserContext は UserProvider の中で使ってください。」 というメッセージが出る(さっき throw したやつ)

👉 **「あ、Provider で囲んでなかったんだな」**とすぐ分かるので、 バグの場所を見つけやすくなります 💡


9️⃣ よくある「別解」と、この教材でのオススメ ✅ / ❌

TypeScript + Context で、null 問題をどう扱うかはいくつか流派があります。

❌ 型アサーションでゴリ押しするパターン

const userContext = useContext(UserContext)!;
// か
const userContext = useContext(UserContext) as UserContextValue;
  • TypeScriptには怒られない
  • でも ほんとうに null が来たら即クラッシュ 💥
  • どこで間違ってるのか分かりにくい

→ この教材では オススメしません

✅ カスタムフックでチェックしてあげるパターン(今やったやつ)

export function useUserContext(): UserContextValue {
const context = useContext(UserContext);

if (context === null) {
throw new Error("useUserContext は UserProvider の中で使ってください。");
}

return context;
}
  • 型的にも安全(戻り値は UserContextValue
  • 実行時にも安全(ミスってたらメッセージ付きでエラー)
  • コンポーネント側はスッキリ ✨

TypeScript の React チートシート的な資料でも 「ランタイムチェック+カスタムフック」のパターンがよく紹介されています。(GitHub)


🔟 ちょこっと応用:いろんな Context で同じパターンを使う 🌈

このカスタムフックのアイデアは、他の Context にもそのまま使えます。

例:テーマ(ライト/ダーク)を管理する ThemeContext を作ったとしたら…

  • useThemeContext
  • useAuthContext
  • useSettingsContext

…など、それぞれの Context ごとに 「安全なカスタムフック」をセットで作る というのが、かなり実務っぽいパターンです 💼


1️⃣1️⃣ 練習タイム 📝(軽めでOK)

時間があるときに、こんな感じで手を動かしてみてください ✋

  1. ThemeContext を作ってみる

    • theme: "light" | "dark"
    • toggleTheme: () => void
    • を持つ ThemeContextThemeProvider を作る
    • useThemeContext同じパターンで作る
  2. ThemeContext を使って

    • ページの背景色や文字色を切り替えるコンポーネントを作る
    • useThemeContext を使って、theme を読み取る
  3. わざと ThemeProvider で囲むのを忘れてみて、

    • どんなエラーが出るか
    • そのメッセージで原因が分かりやすいか
    • を確認してみる

まとめ 🌸

この章で覚えてほしいことはただひとつ:

useContext にそのまま触らず、カスタムフック経由にして null チェックを1か所に集める」

これだけで、

  • コンポーネントが読みやすくなる ✨
  • TypeScript の型もスッキリする 💻
  • ミスしたときにエラーで教えてくれる 🐞

という、いいことづくめです。

次の章からも、Context を使うときは **「Context本体 + Provider + 安全なカスタムフック」**の3点セットを 基本形として使っていきますよ〜 🎉