React・動きをつける

状態を親に持ち上げる

カードの中に閉じこもった状態は、外から数えることも絞りこむこともできません。共通の親へ状態を移し、値と更新用の関数をpropsで配る——Reactでいちばん出番の多い設計の型を、お気に入り機能で身につけます。

前回までで、星を押すとカードが★になります。ここで新しい要求を出します。

「お気に入りだけ見る」チェックボックスを付けたい。あと、上に「お気に入りは◯羽」と出したい。

いまの形では、どちらも作れません

なぜ作れないのか

likedBirdCardの中にあります。カード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ステップで整理されていて、この回でやったことと同じ形です

このレッスンのまとめ

  1. 子の中のstateは親から見えない——共有したいなら共通の親へ持ち上げる
  2. 親は値(isFavorite)と関数(onToggleFavoriteをpropsで配る
  3. 子は関数を呼ぶだけ。データは親から子への一方通行のまま
  4. 配列の更新はincludesで判定、filterで削除、スプレッドで追加
  5. 表示(★・件数・絞りこみ)は1つの配列から全部計算する=ズレようがない
  6. 置き場所は使う部品すべての、いちばん近い共通の親。上げすぎない

次は、ここまでで出そろったエラーの読み方です。state・イベント・一覧がそろった今が、いちばん学びどきです。

やってみよう:お気に入りを親にまとめる

likedBirdCardからAppへ移し、「お気に入りだけ見る」を足します。

  1. BirdCard.jsxからuseStateliked消すimportも消す)
  2. 代わりに{ bird, isFavorite, onToggleFavorite }を受け取り、ボタンでonToggleFavorite(bird.id)を呼ぶ
  3. App.jsxfavoriteIdsonlyFavoritesの2つのstateを足す
  4. toggleFavorite関数を書き、<BirdCard>isFavoriteonToggleFavoriteを渡す
  5. チェックボックスを置き、filterをもう1つつなぐ
const [keyword, setKeyword] = useState('')
const [favoriteIds, setFavoriteIds] = useState([])
const [onlyFavorites, setOnlyFavorites] = useState(false)

function toggleFavorite(id) {
  setFavoriteIds((current) =>
    current.includes(id)
      ? current.filter((favoriteId) => favoriteId !== id)
      : [...current, id],
  )
}

const shownBirds = birds
  .filter((bird) => bird.name.includes(keyword) || bird.area.includes(keyword))
  .filter((bird) => (onlyFavorites ? favoriteIds.includes(bird.id) : true))
<label className="flex items-center gap-2 text-sm text-slate-600">
  <input
    type="checkbox"
    checked={onlyFavorites}
    onChange={(event) => setOnlyFavorites(event.target.checked)}
  />
  お気に入りだけ見る
</label>
「お気に入りだけ見る」にチェックが入っていて、シマエナガとハクチョウの2枚だけが★付きで表示されている。上部に「全8羽のうち、お気に入りは2羽」と出ている
星を付けた鳥だけが残りました。件数も同時に増えているのがポイントです

チェックを入れたまま星を外すと、その場でカードが消えます。1つの配列から全部を計算しているからです。

よくある質問

stateを親に持ち上げるとはどういうことですか?
子コンポーネントが持っていたstateを、共通の親に移すことです。親が値を持ち、子には現在の値と更新用の関数をpropsで渡します。複数の子で同じ情報を使いたいときの基本の形です。
子から親のstateを直接変えられますか?
できません。データは親から子への一方通行です。代わりに、親が「押されたときに呼んでほしい関数」をpropsとして渡し、子はそれを呼びます。
stateはどこに置けばいいですか?
その値を使うコンポーネントすべての、いちばん近い共通の親です。低すぎると共有できず、高すぎると関係ない部分まで描き直されます。迷ったら、まず使う場所を全部あげて共通の親を探します。
propsのバケツリレーが深くなったらどうしますか?
3階層を超えて渡し続けるのがつらくなったら、Contextという仕組みで途中を飛ばせます。ただし多くの画面では、コンポーネントの分け方を見直すほうが先です。