React・動きをつける

useStateで状態を持つ

ふつうの変数を書きかえても画面は変わりません。変化する値はuseStateで持ちます。宣言の書き方、更新すると再描画される仕組み、直接書きかえてはいけない理由、Hooksを呼ぶ場所のルールまで扱います。

ここから画面が動き出します。カードにお気に入りの星を付けて、押したらが切りかわるようにします。

まず、やってはいけない書き方から見ます。

function BirdCard({ bird }) {
  let liked = false                       // ✕

  return (
    <button onClick={() => { liked = true }}>
      {liked ? '★' : '☆'}
    </button>
  )
}

これは動きません。理由は2つあります。

  1. likedを変えても、Reactは画面を描き直さない——書きかわったことに気づけない
  2. 描き直されたとしても、let liked = falseが最初から実行される——値は毎回falseに戻る

コンポーネントはただの関数でした。呼ばれるたびに中身が最初から実行されるので、関数の外に値を覚えておく場所が要ります。それがuseStateです。

useStateの形

import { useState } from 'react'

function BirdCard({ bird }) {
  const [liked, setLiked] = useState(false)

}

読み方はこうです。

  • useState(false)——初期値がfalseの状態を作る
  • liked——いまの値
  • setLiked——値を更新するための関数

[liked, setLiked]は配列の分割代入でした。useStateは「値」と「更新関数」の2つを配列で返してくるので、順番に名前を付けています。名前は自由ですが、[名前, set名前]にそろえるのが慣習です。

更新はこうです。

<button onClick={() => setLiked(!liked)}>
  {liked ? '★' : '☆'}
</button>

setLikedを呼ぶと、Reactはその部品をもう一度実行して、画面を描き直します。今度はlikedが新しい値になっているので、表示が切りかわります。

直接書きかえない

数値や文字列ならsetCount(count + 1)で済みますが、配列とオブジェクトには注意が必要です。

// ✕ 元の配列を書きかえている=Reactからは「変わっていない」ように見える
favorites.push(bird.id)
setFavorites(favorites)

// ○ 新しい配列を作って渡す
setFavorites([...favorites, bird.id])

オブジェクトも同じです。

// ✕
user.name = 'しまちゃん'
setUser(user)

// ○
setUser({ ...user, name: 'しまちゃん' })

Reactは前の値と新しい値が別物かどうかで描き直すかを決めます。pushは中身をいじるだけで同じ配列のままなので、「変わっていない」と判断されてしまうのです。

更新しても、その場では変わらない

これは知らないと必ず引っかかります。

function handleClick() {
  setLiked(true)
  console.log(liked)   // false のまま!
}

setLiked(true)を呼んだ直後なのに、likedは古い値のままです。バグではありません。

その回の実行の中では、stateの値は固定されているからです。likedは「この描画の時点での値」を写し取ったもので、新しい値が入るのは次に部品が呼び直されたときです。

必要なら、変数に入れてから使います

function handleClick() {
  const next = !liked
  setLiked(next)
  console.log(next)   // ○ 新しい値はこちらで持っておく
}

連続で更新するときは関数を渡す

同じstateを立て続けに更新すると、思ったとおりにならないことがあります。

setCount(count + 1)
setCount(count + 1)   // 2ではなく1しか増えない

countはその回の実行時点の値のまま(たとえば0)なので、どちらもsetCount(1)になってしまうためです。「いまの値を受け取って、新しい値を返す関数」を渡すと確実です。

setCount((current) => current + 1)
setCount((current) => current + 1)   // ちゃんと2増える

このコースでも後の回(お気に入りの追加・削除)でこの書き方を使います。迷ったら関数を渡す形にしておけば間違いありません。

stateはカードごとに別々

BirdCardの中でuseStateを書くと、カード1枚ごとに独立した状態を持ちます。8枚並べれば8つのlikedがあり、シマエナガの星を押してもスズメには影響しません。

「同じ部品なのに状態は別々」——これは部品が使われた回数だけ、それぞれ箱が用意されると考えると腑に落ちます。

Hooksを呼ぶ場所のルール

useStateのようにuseで始まる関数をHooks(フック)と呼びます。呼ぶ場所に決まりがあります。

function BirdCard({ bird }) {
  const [liked, setLiked] = useState(false)   // ○ 関数の先頭

  if (!bird) {
    const [x, setX] = useState(0)             // ✕ 条件の中
  }

}

Reactは呼ばれた順番でstateを覚えているので、条件やループで順番が変わると中身が入れかわってしまいます。

  • コンポーネントの先頭に並べて書く
  • iffor・関数の中に入れない

この2つだけ守れば大丈夫です。破ると開発中に赤いエラーで教えてくれます。

公式ドキュメントも見てみる

ja.react.dev外部サイトstate:コンポーネントのメモリ – React公式の「state:コンポーネントのメモリ」。「通常の変数では不十分な場合」の節が、この回の冒頭で見た「動かない書き方」の話そのものです

このレッスンのまとめ

  1. ふつうの変数は書きかえても描き直されず、実行のたびに初期値へ戻る
  2. const [値, set値] = useState(初期値)——値と更新関数を受け取る
  3. 更新関数を呼ぶと部品が呼び直され、画面が描き直される
  4. 更新しても、その回のlikedは古いまま——新しい値は変数に入れて使う
  5. 配列・オブジェクトは直接書きかえない。スプレッド構文で新しく作って渡す
  6. 連続して更新するときはset値((current) => …)の形が確実
  7. stateは部品が使われた回数だけ独立して存在する
  8. Hooksはコンポーネントの先頭で呼ぶ。条件やループの中で呼ばない

次は、イベントと入力です。検索ボックスに文字を打つと、一覧がその場でしぼりこまれるようにします。

やってみよう:お気に入りの星を付ける

BirdCardに、押すと切りかわる星を足します。

  1. src/components/BirdCard.jsxの先頭にimport { useState } from 'react'を書く
  2. 関数の先頭でconst [liked, setLiked] = useState(false)を宣言する
  3. カードの右端にボタンを置き、onClicksetLiked(!liked)を呼ぶ
  4. ボタンの中身を{liked ? '★' : '☆'}にする
import { useState } from 'react'

function BirdCard({ bird }) {
  const { name, emoji, area, memo, rare } = bird
  const [liked, setLiked] = useState(false)

  return (
    <div className="flex items-start gap-4 rounded-2xl bg-white p-5 shadow-sm">
      <span className="text-4xl">{emoji}</span>
      <div className="grow">
        <div className="flex items-center gap-2">
          <h2 className="font-bold text-slate-800">{name}</h2>
          <span className="rounded-full bg-sky-100 px-2 py-0.5 text-xs text-sky-700">
            {area}
          </span>
          {rare >= 3 && (
            <span className="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-bold text-amber-700">
              レア
            </span>
          )}
        </div>
        {memo ? (
          <p className="mt-1 text-sm text-slate-600">{memo}</p>
        ) : (
          <p className="mt-1 text-sm text-slate-400 italic">せつめいはじゅんび中</p>
        )}
      </div>
      <button
        type="button"
        onClick={() => setLiked(!liked)}
        aria-label={`${name}をお気に入りにする`}
        className="text-2xl text-amber-400"
      >
        {liked ? '★' : '☆'}
      </button>
    </div>
  )
}

export default BirdCard
8羽のカードが並び、シマエナガとフクロウの星が黄色く塗りつぶされた★になっている。ほかの鳥は白抜きの☆のまま
押した鳥だけが★に変わります。カードごとに状態が独立していることも、いくつか押して確かめてください

aria-labelを付けているのは、星だけのボタンだと読み上げたときに何のボタンか分からないためです。見た目に文字が無いボタンには、必ず付けるようにします。

よくある質問

useStateとは何ですか?
コンポーネントが「変化する値」を覚えておくための仕組みです。現在の値と、それを更新するための関数の2つを返します。更新関数を呼ぶと、Reactがその部品を描き直します。
ふつうの変数ではなぜだめなのですか?
書きかえても再描画が起きないからです。さらに、再描画のたびに関数が最初から実行されるので、ふつうの変数は初期値に戻ってしまいます。値を覚えておくにはuseStateが必要です。
stateを更新したのに画面が変わりません
配列やオブジェクトを直接書きかえている可能性が高いです。pushやプロパティへの代入は元をこわすだけで、Reactからは同じものに見えます。スプレッド構文で新しい配列やオブジェクトを作って渡してください。
useStateはif文の中に書いてもいいですか?
いけません。Hooksは常に同じ順番で呼ばれる前提で作られているので、条件やループの中に置くと順番が変わって壊れます。コンポーネントの先頭に並べて書きます。