【実務・中級編】 複数useStateとオブジェクト状態の使い分け – React実践ガイド

おい、調子はどうだい?
今日も元気に `useState` の無限ループと格闘していることだろう。

おっと、そんな険しい顔をするなよ。今回はな、中級のエンジニアたちが必ず一度はハマり、そして「一体どっちで書くのが正解なんだ…?」と夜も眠れなくなる永遠のテーマについて、そろそろ決着をつけようと思う。

そう、「関連する状態を1つのオブジェクトにまとめるべきか、個別の `useState` に細かく分けるべきか」だ。

ネットの海を漂う適当なチュートリアルを見ると、「オブジェクトでまとめろ」と言ったり、「いやいやアトミックに個別に分けろ」と言ったり、言ってることがバラバラで頭が痛くなるよな。でも、シニアの現場視点から言わせてもらうと、「なんとなく」で選んでいいコードなんて一行もない。 ブラウザの描画の仕組みや、Reactのファイバーアーキテクチャの裏側を理解していれば、自ずと「正しい選択」は一つに絞られてくるんだ。

さあ、コーヒーでも飲みながら、現場のリアルな知見に満ちた設計の極意を覗いていこうか。

—

1. なぜこの設計でエンジニアは迷うのか?

フォームの入力値や、検索フィルターの条件などを管理するとき、君たちはどう書いているだろうか?

パターンA:オブジェクトでまとめる流儀

const [form, setForm] = useState({ name: ”, email: ”, age: 0 });

パターンB:個別に `useState` を乱立させる流儀

const [name, setName] = useState(”);
const [email, setEmail] = useState(”);
const [age, setAge] = useState(0);

一見すると、どちらでも動く。だからこそ厄介だ。
しかし、ここにはReact特有の「状態更新のバグ」「不要な再レンダリング(パフォーマンス低下)」という地雷が埋まっている。それぞれの裏側の挙動を知れば、どちらをいつ使うべきかが見えてくる。

—

2. ブラウザとReactの裏側で何が起きているのか?

まず大前提として、Reactの `useState` の更新は非同期(厳密にはバッチ処理される)だ。
複数のステート更新がキューイングされ、Reactが「よし、今だ!」と判断したタイミングでまとめてコンポーネントを再描画(レンダリング)する。

オブジェクト状態(パターンA)の罠

オブジェクトで状態を持つ場合、最大の注意点は「スプレッド構文(`…`)によるシャローコピーのし忘れ」と「不要なプロパティの変更による全域再レンダリング」だ。

// やりがちなバグの温床
setForm({ name: ‘Taro’ }); // ああ! email と age が消滅した!

正しくはこう書く必要がある:

setForm(prev => ({ …prev, name: ‘Taro’ }));

さらにパフォーマンス面でのペルソナとして、`form` オブジェクトの中の `name` が1文字変わっただけでも、`email` や `age` しか使っていない(あるいは全く関係ない)子コンポーネントまで巻き込んで再レンダリングが走るリスクがある。React.memoをサボっていれば、アプリ全体が重くなる原因の直撃弾だ。

個別useState(パターンB)の罠

じゃあ、全部バラバラにすればいいじゃないか、と思うだろ?
甘い。個別に分けると、今度は「状態の同期ズレ」と「コードの肥大化」という魔物が顔を出す。

例えば、バリデーションエラーのメッセージ群を考えてみてくれ。

const [nameError, setNameError] = useState(”);
const [emailError, setEmailError] = useState(”);
const [ageError, setAgeError] = useState(”);
// 送信ボタンを押したときのハンドラがカオスになる…

「AとBの値が同時に変わったとき、整合性が取れていなければならない」というビジネスロジックがある場合、個別の `useState` だと、片方だけ更新されてもう片方が古いままの「中間状態」が生まれる隙ができてしまう。これが実務で最も恐ろしい、原因特定が困難なバグの温床になるんだ。

—

3. 現場で使える設計指針:判断基準のフレームワーク

じゃあ、俺たちはどう使い分ければいいのか?
シニアとして、チームに展開している鉄則の判断基準を授けよう。

1. 「一緒に更新され、一緒に消え、密接に結合しているデータ」は1つのオブジェクトにまとめろ。

  • 例:ユーザーのプロフィール情報、画面のフィルタリング条件セット、座標(x, y)。これらはバラバラに存在すると意味をなさない。

2. 「完全に独立しており、お互いの存在を知る必要がない単一の値」は個別に分けろ。

  • 例:モーダルの開閉フラグ(`isOpen`)、タブの選択中インデックス(`activeTab`)、ローディング状態(`isLoading`)。これらを1つのオブジェクトにまとめるのはナンセンスだ。

—

4. 実戦投入コード:美しくスケーラブルな実装例

百聞は一見にしかずだ。
中規模以上のフォーム画面を想定して、オブジェクト状態を安全に、かつスマートに扱う実務レベルのコードを見せてやろう。コピペして自分のプロジェクトの構成と比較してみてくれ。

import React, { useState, useCallback } from ‘react’;

/

  • ユーザー登録フォームのコンポーネント
  • 関連するフォーム入力値は1つのオブジェクトで管理しつつ、
  • 更新時のボイラープレート(定型コード)をスマートに処理する例

/
export const UserRegistrationForm = () => {
// 1. 関連する入力値は1つのオブジェクトにまとめる
const [formData, setFormData] = useState({
username: ”,
email: ”,
tier: ‘standard’,
});

const [isLoading, setIsLoading] = useState(false); // これは独立しているので個別

// 2. 汎用的な入力ハンドラの作成
// オブジェクトのキーを動的に書き換えることで、input毎にハンドラを作る必要をなくす
const handleChange = useCallback((e) => {
const { name, value } = e.target;

setFormData((prevFormData) => ({
…prevFormData, // 既存のプロパティを必ず展開する(古い値を上書きして消さないため)
[name]: value, // ES6の計算プロパティ名で動的に更新
}));
}, []);

const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);

try {
// ここでAPI通信のモックを実行
console.log(‘送信データ:’, formData);
await new Promise((resolve) => setTimeout(resolve, 1500));
alert(‘登録が完了しました!’);
} catch (error) {
console.error(‘送信エラー:’, error);
} finally {
setIsLoading(false);
}
};

return (

アカウント登録



:input


);
};

このコードのシニア的解説ポイント

  • `name` 属性の活用: HTMLの `name` 属性と `formData` のプロパティ名を一致させることで、一つの `handleChange` 関数だけで全てのテキスト入力をプレフィックスなしでまわせるようにしている。DRY(Don’t Repeat Yourself)原則の美しい適用例だ。
  • 関数型アップデート: `setFormData(prevFormData => …)` を使っている点に注目してほしい。非同期でバッチ処理されるReactにおいて、最新のステートを確実にキャッチするための必須テクニックだ。

—

5. まとめ:プロとしての誇りを持ってコードを書こう

さて、長々と語ってきたが、結論はシンプルだ。

  • 「一緒に変わるべきもの」はオブジェクトにまとめ、スプレッド構文で安全に更新する。
  • 「まったく関係のない単一のフラグや値」は、ケチらずに個別の `useState` に分ける。

そして何より大切なのは、その設計を選んだ理由を、後輩やチームメンバーに自分の言葉で説明できることだ。「なんとなくネットに書いてあったから」ではなく、「ブラウザの再レンダリング最適化と状態の整合性のためにこの形にしたんだ」と胸を張って言えるエンジニアになってほしい。

君の書くコードが、明日も美しく、そしてバグのないものでありますように。
それじゃあ、またコードレビューの現場で会おう!

コメント

タイトルとURLをコピーしました