前回ページを分けたことで、こういう状態になりました。
- 一覧ページ:
fetch('/birds.json')して自分で持っている - 詳細ページ:同じデータをもう一度
fetchしている
動きますが、同じものを2か所で持っているのは気持ちが悪いですし、実害もあります。フォームで鳥を足しても、詳細ページ側のデータには入りません。
propsで渡せないのか?
「持ち上げ」の回でやったように、親が持って子に配る——それが基本でした。でも今回は難しいのです。
<Route path="/" element={<BirdListPage />} />
<Route path="/birds/:id" element={<BirdDetailPage />} />ページは<Route>が呼び出すので、親から手渡しする場所がありません。無理に渡そうとすると、Appが全部のデータを持って、各<Route>のelementに書き並べることになります。
そこでContextです。
Contextは「木のどこからでも読める値」
考え方はシンプルです。
- 入れ物を作る(
createContext) - 上のほうで値を入れて包む(Provider)
- 読みたい場所で取り出す(
useContext)
propsがバケツリレーで手渡しなら、Contextは建物に引いた水道です。途中の部屋を通さずに、蛇口をひねれば出てきます。
作ってみる
図鑑のデータを配るContextを作ります。中身はこれまでのコードの引っ越しで、新しい概念はcreateContextとuseContextの2つだけです。
import { createContext, useContext, useEffect, useState } from 'react'
const BirdsContext = createContext(null)
export function BirdsProvider({ children }) {
const [birds, setBirds] = useState([])
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
fetch('/birds.json')
.then((response) => response.json())
.then((data) => {
setBirds(data)
setIsLoading(false)
})
}, [])
function addBird(newBird) {
setBirds((current) => [...current, newBird])
}
function removeBird(id) {
setBirds((current) => current.filter((bird) => bird.id !== id))
}
return (
<BirdsContext value={{ birds, isLoading, addBird, removeBird }}>
{children}
</BirdsContext>
)
}
export function useBirds() {
const value = useContext(BirdsContext)
if (!value) {
throw new Error('useBirds は BirdsProvider の中で使ってください')
}
return value
}3つのことをしています。
createContext(null)——入れ物を作る。nullは「まだ何も入っていない」ときの値BirdsProvider——データとその操作関数をvalueに入れて、childrenを包むuseBirds——読み出し口。カスタムフックの回の型がそのまま効いています
包んで、読む
包むのはアプリの上のほう。ルーターの内側に置きます。
<BrowserRouter>
<BirdsProvider>
<App />
</BirdsProvider>
</BrowserRouter>読むのは、必要な場所で1行です。
// 一覧ページ
const { birds, isLoading, addBird } = useBirds()
// 詳細ページ
const { birds, isLoading, removeBird } = useBirds()両方のページからfetchが消えました。 データは1か所(Provider)にあり、どちらのページもそこを読むだけです。
これで、フォームで足した鳥は詳細ページからも見えますし、詳細ページで消せば一覧からも消えます。
削除を足してみる
共有できたので、削除が意味を持つようになります。詳細ページに置きます。
import { useNavigate } from 'react-router-dom'
const navigate = useNavigate()
function handleRemove() {
removeBird(bird.id)
navigate('/') // 消したページに留まれないので一覧へ
}useNavigateはコードから移動するための道具です。<Link>はユーザーが押すもの、useNavigateは処理の中で移動したいとき——という使い分けになります。
削除そのものはfilterの1行です。「元をこわさず新しい配列を作る」——ここまで何度も出てきた形が、そのまま使えます。
何でもContextに入れない
便利なので入れすぎたくなりますが、注意点があります。
判断の目安はこうです。
| 状況 | どうする |
|---|---|
| 親子で受け渡すだけ | props(値の流れが目に見えるので、こちらが基本) |
| 3階層を超えて渡し続けている | Contextを検討 |
| 別のページからも同じ値を読む | Context(今回がこれ) |
| ログイン中のユーザー・テーマ・言語 | Contextの定番 |
| 1つの画面の中だけで使う値 | その画面のstateのまま |
公式ドキュメントも見てみる
ja.react.dev外部サイトコンテクストで深くデータを受け渡す – React公式の「コンテクストを使って深くデータを受け渡す」。「コンテクストを使う前に」の節が、この回の判断の目安と同じことを言っていますこのレッスンのまとめ
- ページを分けると同じデータを2か所で持つ問題が出る——Contextの出番
- 手順は入れ物を作る(
createContext)→ 値を入れて包む(Provider)→ 取り出す(useContext) - Providerの中身は持ち上げのときのコードそのまま。増えたのは配り方だけ
- 読み出しはカスタムフック(
useBirds)にまとめるとエラーにも気づける - React 19では
<Context value={…}>と書ける(古い記事の.Providerと同じ) - コードから移動したいときは
useNavigate - まずprops、つらくなったらContext。何でも入れると再描画が広がる
次は、Contextだけでは足りなくなったときの選択肢——状態管理ライブラリです。
