[PR] 本記事にはプロモーションが含まれています。

【React】useStateのset関数の使い方|関数で更新する理由、オブジェクトと配列の更新、更新直後に古い値が出るわけ

React

Reactで画面の状態を持つときは、useState を使います。

TypeScript
const [count, setCount] = useState(0);

count が今の値、setCount が値を更新するための関数(set関数)です。この記事では、set関数の使い方と、つまずきやすいところをまとめます。

set関数とは

useState は、今の値と、それを更新する関数の2つを配列で返します。値を変えるときは、count = 1 のように直接代入せず、必ず set関数を使います。set関数を呼ぶと、Reactが新しい値で画面を描き直します。

名前に決まりはありませんが、「set + 変数名」にするのが一般的です。

変数set関数
countsetCount
usersetUser
isModalOpensetIsModalOpen

更新の2つの書き方

新しい値をそのまま渡す

前の値と関係なく、新しい値にするときは、その値を渡します。

TypeScript
setCount(0);           // 0 に戻す
setName('React太郎');  // 入力された文字にする

前の値を使うときは関数を渡す

「今の値に1を足す」のように、前の値をもとにするときは、関数を渡します。Reactが、その時点の最新の値を引数に入れて呼んでくれます。

TypeScript
setCount(prev => prev + 1);

違いが出るのは、同じ処理の中で何度も更新するときです。

TypeScript
// count が 0 のときにクリックすると…

function handleClickA() {
  setCount(count + 1); // 0 + 1
  setCount(count + 1); // この行でも count は 0 のままなので 0 + 1
  setCount(count + 1); // 0 + 1
  // 結果は 1
}

function handleClickB() {
  setCount(prev => prev + 1); // 0 → 1
  setCount(prev => prev + 1); // 1 → 2
  setCount(prev => prev + 1); // 2 → 3
  // 結果は 3
}

関数を渡しておけば、このようなずれが起きません。前の値を使うときは、関数で書くと覚えておくと安全です。

渡した関数は、開発中(StrictMode)に2回呼ばれることがあります。中で通信やログの書き込みなどをしないで、新しい値を計算して返すだけにします。

オブジェクトを更新する

Reactは、set関数に渡された値が前と同じものかどうかで、描き直すかを決めます。オブジェクトの中身だけを書き換えて同じオブジェクトを渡すと、「変わっていない」と判断されて、画面が更新されません。

TypeScript
const [user, setUser] = useState({ id: 1, name: '太郎' });

// × 中身を直接書き換えて、同じオブジェクトを渡している
user.name = '次郎';
setUser(user); // 画面は更新されない

// ○ スプレッド構文で、name だけ変えた新しいオブジェクトを作る
setUser(prev => ({ ...prev, name: '次郎' }));

関数でオブジェクトを返すときは、({ ... }) のように丸かっこで囲みます。囲まないと { } が関数の中身とみなされます。

入れ子のオブジェクトは中もコピーする

スプレッド構文(...)がコピーするのは1段目だけです。中のオブジェクトを変えるときは、そこも新しく作ります。

TypeScript
// user = { name: '太郎', address: { city: '札幌市', zip: '060-0000' } }
setUser(prev => ({
  ...prev,
  address: { ...prev.address, city: '大阪市' },
}));

配列を更新する

配列も同じで、push や splice のように元の配列を変えるメソッドは使わず、新しい配列を作って渡します。

TypeScript
// 追加する
setItems(prev => [...prev, newItem]);

// 削除する(id が一致しないものだけ残す)
setItems(prev => prev.filter(item => item.id !== id));

// 1つだけ変える(その要素だけ新しいオブジェクトにする)
setItems(prev =>
  prev.map(item => (item.id === id ? { ...item, done: !item.done } : item))
);
やりたいこと使わない使う
追加push[...prev, x]
削除splicefilter
変更prev[i] = xmap
並べ替えsort[...prev].sort() または toSorted()

set関数を呼んだ直後は、まだ古い値

TypeScript
function handleClick() {
  setCount(10);
  console.log(count); // 10 ではなく、更新前の値が出る
}

これは set関数が「非同期だから」と説明されることがありますが、正確には少し違います。count は、その回の画面を描いたときの値で決まっていて、その関数の中では変わりません。setCount(10) は「次に画面を描くときは 10 にしてね」と予約するだけで、今の count を書き換えるものではありません。

新しい値をその場で使いたいときは、先に変数に入れておいて、それを使います。

TypeScript
function handleClick() {
  const next = count + 1;
  setCount(next);

  // 新しい値を使う処理は next で行う
  saveToServer(next);
}

「値が変わったら何かする」のを useEffect で書く方法もありますが、クリックなどがきっかけの処理なら、上のようにその処理の中で書くほうが、いつ動くか分かりやすくなります。useEffect は、画面の外の仕組み(タイマーや通信の接続など)と合わせるときに使います。

TypeScript で型を付ける

初めの値から型が分かるとき(useState(0) なら number)は、書かなくても自動で付きます。null や空の配列で始めるときは、<> で型を指定します。

TypeScript
type User = { id: number; name: string };
type Item = { id: number; title: string; done: boolean };

const [user, setUser] = useState<User | null>(null); // まだ読み込んでいないときは null
const [items, setItems] = useState<Item[]>([]);       // 空の配列から始める

型を付けないと、useState([]) は「何も入らない配列」とみなされて、あとで要素を足そうとするとエラーになります。

まとめ

  • 値は直接代入せず、set関数で更新する
  • 前の値を使うときは setCount(prev => prev + 1) のように関数を渡す
  • オブジェクトと配列は、中身を書き換えずに新しく作って渡す。入れ子は中もコピーする
  • set関数を呼んだ直後は、まだ古い値。新しい値は先に変数に入れて使う
  • null や空の配列で始めるときは、useState<型> で型を付ける

ほかの開発の記事は、開発・プログラミングメモにまとめています。

ほかの開発・プログラミングの記事は、開発・プログラミングメモの目次から、テーマ別に探せます。

このブログは、にほんブログ村と人気ブログランキングに参加しています。記事が役に立ったら、下のランキングのバナーのどれかを押して応援してもらえると励みになります。

ブログランキング・にほんブログ村へにほんブログ村

サーバー構築・運用ランキング プログラムランキング

React
watasoftdevをフォローする

コメント