前回までで、星を押すとカードが★になります。ここで新しい要求を出します。
「お気に入りだけ見る」チェックボックスを付けたい。あと、上に「お気に入りは◯羽」と出したい。
いまの形では、どちらも作れません。
なぜ作れないのか
likedはBirdCardの中にあります。カード1枚ごとに独立した箱です。
function BirdCard({ bird }) {
const [liked, setLiked] = useState(false) // ← ここに閉じこもっている親のAppから見ると、8枚のカードがそれぞれ何を持っているのか分かりません。データは親から子への一方通行なので、子の中身を親が覗くことはできないのです。
親に持ち上げる
Appが「お気に入りのidの配列」を1つ持ちます。
const [favoriteIds, setFavoriteIds] = useState([])カード側のuseStateは削除します。代わりに2つのpropsを受け取ります。
| props | 中身 | 向き |
|---|---|---|
isFavorite | この鳥がお気に入りか(true/false) | 親 → 子 |
onToggleFavorite | 押されたときに呼ぶ関数 | 親 → 子(呼ぶのは子) |
値は下に流れ、知らせは関数を通じて上に返る——この形がReactの基本です。
// 親(App.jsx)
<BirdCard
key={bird.id}
bird={bird}
isFavorite={favoriteIds.includes(bird.id)}
onToggleFavorite={toggleFavorite}
/>// 子(BirdCard.jsx)
<button onClick={() => onToggleFavorite(bird.id)}>
{isFavorite ? '★' : '☆'}
</button>子は自分が何番目の鳥かを知っているので、bird.idを持たせて呼び返します。親はそのidを受け取って、配列を更新します。
配列のstateを更新する
お気に入りの追加と削除は、同じ関数で切りかえます。
function toggleFavorite(id) {
setFavoriteIds((current) =>
current.includes(id)
? current.filter((favoriteId) => favoriteId !== id) // 入っていたら外す
: [...current, id], // 入っていなければ足す
)
}includes——その値が配列にあるかfilter——条件に合うものだけ残した新しい配列(=削除)[...current, id]——末尾に足した新しい配列(=追加)
どちらも元の配列をこわしていません。前回の「直接書きかえない」がそのまま効いています。
更新関数に関数を渡す形((current) => …)にしているのは、いまの値をもとに次の値を作るからです。連続で押されても取りこぼしません。
絞りこみを重ねる
「検索」と「お気に入りだけ」は、filterを2回つなぐだけです。
const shownBirds = birds
.filter((bird) => bird.name.includes(keyword) || bird.area.includes(keyword))
.filter((bird) => (onlyFavorites ? favoriteIds.includes(bird.id) : true))2つめのfilterは、チェックが外れているときはtrueを返す=素通しです。条件が増えても、この形で足していけます。
件数も配列から出せます。
<p>全{birds.length}羽のうち、お気に入りは{favoriteIds.length}羽</p>持ち上げすぎにも注意
「じゃあ全部Appに置けばいい」とはなりません。
- 上に置くほどその値を使わない部分まで描き直される
Appが肥大化して、何を持っているのか読めなくなる- 途中の部品が、自分では使わないpropsをただ下へ渡すだけになる(バケツリレー)
置き場所は「その値を使う部品すべての、いちばん近い共通の親」。カードの中で完結する状態(説明の開閉など)は、カードに置いたままで正解です。
公式ドキュメントも見てみる
ja.react.dev外部サイトコンポーネント間で state を共有する – React公式の「コンポーネント間でstateを共有する」。「stateのリフトアップ」の手順が3ステップで整理されていて、この回でやったことと同じ形ですこのレッスンのまとめ
- 子の中のstateは親から見えない——共有したいなら共通の親へ持ち上げる
- 親は値(
isFavorite)と関数(onToggleFavorite)をpropsで配る - 子は関数を呼ぶだけ。データは親から子への一方通行のまま
- 配列の更新は
includesで判定、filterで削除、スプレッドで追加 - 表示(★・件数・絞りこみ)は1つの配列から全部計算する=ズレようがない
- 置き場所は使う部品すべての、いちばん近い共通の親。上げすぎない
次は、ここまでで出そろったエラーの読み方です。state・イベント・一覧がそろった今が、いちばん学びどきです。
