ここから画面が動き出します。カードにお気に入りの星を付けて、押したら☆と★が切りかわるようにします。
まず、やってはいけない書き方から見ます。
function BirdCard({ bird }) {
let liked = false // ✕
return (
<button onClick={() => { liked = true }}>
{liked ? '★' : '☆'}
</button>
)
}これは動きません。理由は2つあります。
likedを変えても、Reactは画面を描き直さない——書きかわったことに気づけない- 描き直されたとしても、
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を覚えているので、条件やループで順番が変わると中身が入れかわってしまいます。
- コンポーネントの先頭に並べて書く
if・for・関数の中に入れない
この2つだけ守れば大丈夫です。破ると開発中に赤いエラーで教えてくれます。
公式ドキュメントも見てみる
ja.react.dev外部サイトstate:コンポーネントのメモリ – React公式の「state:コンポーネントのメモリ」。「通常の変数では不十分な場合」の節が、この回の冒頭で見た「動かない書き方」の話そのものですこのレッスンのまとめ
- ふつうの変数は書きかえても描き直されず、実行のたびに初期値へ戻る
const [値, set値] = useState(初期値)——値と更新関数を受け取る- 更新関数を呼ぶと部品が呼び直され、画面が描き直される
- 更新しても、その回の
likedは古いまま——新しい値は変数に入れて使う - 配列・オブジェクトは直接書きかえない。スプレッド構文で新しく作って渡す
- 連続して更新するときは
set値((current) => …)の形が確実 - stateは部品が使われた回数だけ独立して存在する
- Hooksはコンポーネントの先頭で呼ぶ。条件やループの中で呼ばない
次は、イベントと入力です。検索ボックスに文字を打つと、一覧がその場でしぼりこまれるようにします。
