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

【React】useStateを極める:setValとsetDataの正しい使い方とよくある落とし穴

React

Reactでコンポーネントの状態(State)を管理する際、最も頻繁に使用されるのがuseStateフックです。

const [val, setVal] = useState(0)
const [data, setData] = useState({})

のように宣言しますが、この後半部分であるsetValsetData(セッター関数)の挙動を正確に理解することは、予期せぬバグを防ぐために非常に重要です。

本記事では、状態更新関数の基本から、オブジェクトや配列を扱う際の注意点、そして多くの人がつまずく非同期的な挙動について詳しく解説します。

そもそもsetValやsetDataとは?

これらはuseStateが返す状態を更新するための専用関数(セッター関数)です。
Reactのルールとして、状態変数(valdata)に直接代入して値を変更することは禁止されています。必ずこのセッター関数を通して値を更新する必要があります。

名前に決まりはありませんが、慣例として「set + 状態変数名(キャメルケース)」とするのが一般的です。

  • val を更新する関数 → setVal
  • data を更新する関数 → setData
  • isModalOpen を更新する関数 → setIsModalOpen

状態を更新する2つのアプローチ

セッター関数の使い方には、大きく分けて「直接値を渡す方法」と「関数を渡す方法」の2種類があります。

1. 値を直接指定する

状態を新しい値で完全に上書きしたい場合に使用します。ユーザーの入力値をそのままセットする場合などに適しています。

TypeScript
// カウンターをリセットする
setVal(0);

// 文字列を更新する
setName("React太郎");

2. 関数アップデート(前の状態を利用する)

「現在の値に1を足す」など、前の状態に依存した更新を行う場合は、コールバック関数を渡すのが安全です。

TypeScript
// 現在のvalに1を足す
setVal(prevVal => prevVal + 1);

なぜ関数を渡す必要があるのか?
Reactはパフォーマンス最適化のため、複数の状態更新をまとめて処理(バッチ処理)することがあります。
直接値を指定する方法で連続して更新すると、古い状態を参照してしまう「ステートの不整合」が起きる可能性があります。
関数アップデートを使えば、Reactが常に最新の状態(prevVal)を引数として渡してくれるため、この問題を回避できます。

setData:オブジェクトや配列の更新(ミューテーションの回避)

変数名がdataの場合、中身がオブジェクトや配列などの複雑なデータ構造であることが多いです。ここで注意すべきなのが「イミュータビリティ(不変性)の保持」です。

Reactは、セッター関数に渡された値の「参照」が以前と変わっているかどうかで、再レンダリングすべきかを判断します。
オブジェクトの中身だけを直接書き換えても、Reactは変更を検知できません。

❌ 悪い例(Reactが変更を検知できない)

TypeScript
const [data, setData] = useState({ id: 1, name: "太郎" });

const updateName = () => {
  data.name = "次郎"; // 直接オブジェクトを書き換えている(ミューテーション)
  setData(data);      // 参照先が同じなので画面は更新されない
};

⭕ 良い例(スプレッド構文を使って新しいオブジェクトを作る)

TypeScript
const updateName = () => {
  setData(prevData => {
    // 以前のデータを展開し、nameだけを上書きした「新しいオブジェクト」を返す
    return { ...prevData, name: "次郎" };
  });
};

配列の場合も同様に、pushspliceなどの元の配列を変更するメソッドは避け、スプレッド構文(...)やmapfilterを使って新しい配列を生成してからsetDataに渡します。

よくある落とし穴:セッター関数は「非同期的」に動く

セッター関数を実行した直後にconsole.logで状態を確認しようとして、値が更新されておらず焦った経験はないでしょうか。

TypeScript
const updateValue = () => {
  setVal(10);
  console.log(val); // 10ではなく、更新前の古い値が出力される!
};

Reactのセッター関数は、呼び出された瞬間に変数の値を書き換えるわけではありません。
「次回のレンダリング時にこの値にしてね」とReactに予約を入れているようなイメージです。そのため、同じ関数内の次の行では、まだ古い値のままになっています。

更新後の値をすぐに使って別の処理をしたい場合は、useEffectを利用して状態の変更を監視するか、更新に使用した新しい値を変数として保持しておく必要があります。

TypeScript
const updateValue = () => {
  const newValue = 10;
  setVal(newValue);
  
  // 更新用の値(newValue)をそのまま別の処理に使う
  doSomethingElse(newValue); 
};

コメント