- Reactの状態は不変として扱われるべきであり、特にオブジェクトや配列については、直接的な変更ではなくセッターを介して更新を行う必要がある。
- 状態の更新は非同期であり、バッチ処理される可能性があるため、関数型アップデーターを使用することで、タイマー、クロージャ、および高速なインタラクションにおける古い状態の問題を防ぐことができます。
- フック(useState、useRefなど)を備えた関数コンポーネントは現代の標準であり、React.memoやImmerのようなツールはパフォーマンスとネストされたデータの処理に役立ちます。
- プロパティとステートを明確に分離し、トップダウンのデータフローモデルを採用することで、アプリケーションの規模が拡大してもコンポーネントの動作を予測可能に維持できます。
ステートは、一見シンプルに見えるReactの概念の一つですが、アプリが大きくなるにつれてすぐに複雑になっていきます。 最初は小さなカウンターから始まるかもしれませんが、突然、複数のフォームフィールド、非同期更新、ネストされたオブジェクト、そしてすべてが一度に再レンダリングされる際のパフォーマンス問題など、様々な問題に直面することになります。状態を深く理解することこそが、「Reactを使っている」だけの人と、実際のReactアプリケーションを拡張し、デバッグできる人との違いを生み出すのです。
このガイドでは、クラスコンポーネントやライフサイクルメソッドから、最新のHooksやイミュータブルな更新まで、Reactの現状(文字通りの意味で)を順を追って解説します。 非同期更新、古いクロージャ、useStateの代わりにuseRefを使うべき場合、UIの予測可能性を維持する方法など、微妙ながらも重要なトピックについても掘り下げていきます。目標は、コンポーネントが期待どおりに動作するように、明確なメンタルモデルを提供することです。
小道具から国家まで:一体何がどこに属すべきなのか?
すべてのReactコンポーネントの中核には、propsとstateという2つの主要なデータソースがあります。 小道具 親コンポーネントから渡され、そのレンダリングのライフサイクル中は固定されますが、 状態 これはコンポーネント自体によって所有および管理され、時間の経過とともに変化するデータを対象としています。
一般的な目安としては、データが外部から設定され、このコンポーネント内で変更されない場合はプロパティであり、コンポーネントがそれを追跡および更新する必要がある場合はステートである、ということが挙げられます。 点滅するテキストコンポーネントを想像してみてください。実際のテキストは一度だけ(プロパティとして)提供されますが、それが現在表示されているか非表示になっているかは継続的に切り替わります(状態)。この区別こそが、Reactがデータフローを予測可能で一方向に保つことを可能にするのです。
Reactは、状態がそれを制御する必要のある最も近い共通の祖先に保持される、トップダウン(単方向)のデータフローを推奨しています。 親コンポーネントは状態を保持し、値をプロパティとして子コンポーネントに渡すことができます。子コンポーネントはそれらの値をレンダリングしたり変換したりできますが、それらの値が元々状態、他のプロパティ、またはハードコーディングされたものから来たかどうかを知る必要はありません。
そのため、状態は「ローカル」または「カプセル化されている」とよく言われるのです。 状態を所有するコンポーネントのみがその状態を変更でき、その状態から派生したUIはpropsを通じて下位コンポーネントへと伝播します。ステートフルコンポーネントとステートレス(純粋)コンポーネントは自由に組み合わせることができ、何かがステートフルであるかどうかは実装の詳細であり、時間の経過とともに変化する可能性があります。
クラスコンポーネント:昔ながらの方法で状態とライフサイクルを管理する
Hooksが登場する以前は、Reactで状態管理やライフサイクル管理のメソッドを使用する唯一の方法は、ES6のクラスコンポーネントを使うことでした。 現代のアプリのほとんどは関数コンポーネントに依存していますが、多くのコードベースでは依然としてクラスコンポーネントが使われており(場合によっては保守も必要になります)、その仕組みを理解しておくことは重要です。
単純な関数コンポーネントを変換するには Clock 授業に参加するには、いくつかの手順を踏む必要があります。 拡張するクラスを作成します React.Component、 render() メソッド、関数本体を移動 render、交換 props this.props元の関数を削除します。React がレンダリングを続ける限り <Clock /> 同じ DOM ノード内で使用される場合、そのクラスの単一のインスタンスが再利用されます。
クラスにローカル状態を追加するということは、コンストラクタを定義し、初期値を割り当てることを意味します。 this.state オブジェクト。 例えば、 date props から state に値を取得するには、コンストラクタを追加して呼び出します。 super(props) とセット this.state = { date: new Date() }使用箇所をすべて置き換える this.props.date in render() this.state.dateクラスコンポーネントでは、直接割り当てるのは this.state コンストラクター内。
ライフサイクルメソッドは、コンポーネントのライフサイクルの特定の時点でReactが呼び出す特別なクラスメソッドです。 コンポーネントが最初にDOMに挿入(マウント)されると、Reactは componentDidMount(). 削除(アンマウント)されると、React は を呼び出します componentWillUnmount()古典的な時計の例では、タイマーを次のように設定します。 componentDidMount そしてそれをクリアする componentWillUnmountタイマーIDを保存 this (例えば this.timerId)、そして電話 this.setState() 毎秒時刻を更新します。
その時計の典型的なライフサイクルは次のようになります。 React はコンストラクタを呼び出して状態を初期化し、 render() DOMを生成するには、 componentDidMount() タイマーを開始する場所。タイマーが作動するたびに、呼び出します。 setState()更新をキューに入れてトリガーします render() 新しい状態になります。コンポーネントが削除されると、 componentWillUnmount() タイマーをクリアして、リソースのリークを防ぎます。
クラスで状態を正しく管理するということは、次の3つの重要なルールを尊重することも意味します。 setState. 突然変異してはならない this.state 直接的に、更新は非同期かつバッチ処理される可能性があることを覚えておく必要があり、更新は浅くマージされる(最上位の状態キーのみがマージされ、深くネストされたオブジェクトはマージされない)ことを理解しておく必要があります。
状態を正しく使用する:ミューテーション、非同期更新、データフロー
初心者にとって最も混乱を招く原因の一つは、 setState (およびHookの同等の機能)は状態を即座に更新しないため、状態オブジェクトをその場で変更してはいけません。 React はパフォーマンスのために複数の更新をまとめて処理することが多いので、 this.state クラスや Hooks の状態変数は、更新をスケジュールした直後の最終状態を反映しない場合があります。
状態を直接変更する、例えば this.state.count++ または状態オブジェクトのプロパティを変更すると、React の変更検出がスキップされ、コンポーネントが古い値のままになることがあります。 Reactでは、ステート内のオブジェクトはすべて読み取り専用として扱うことが求められます。既存のオブジェクトを変更するのではなく、必要な変更を加えた新しいオブジェクトまたは配列を作成し、それをステートアップデーターに渡します。
状態の更新は非同期で行われる可能性があるため、前の状態から次の状態を計算する際には注意が必要です。 授業では、 this.setState({ count: this.state.count + 1 }) 複数の更新がバッチ処理される場合、誤った結果になる可能性があります。修正方法は、関数形式を使用することです。 this.setState((prevState, props) => ({ count: prevState.count + 1 }))これにより、常に最新の状態スナップショットを使用していることが保証されます。
Hooksでも同様のパターンが存在します。値の代わりに関数を使ってアップデーターを呼び出すことができます。 たとえば、 setCount(prev => prev + 1) 新しい値が前の値に依存する場合、または後で実行されるタイマーやイベントハンドラー内で更新が発生する可能性がある場合は、カウンターをインクリメントするより安全な方法です。
状態は「ローカル」であるにもかかわらず、状態変化の影響は常にコンポーネントツリーを下って伝播する。 状態の更新によって親要素が再レンダリングされると、デフォルトでその子要素もすべて再レンダリングされます。このトップダウンのデータフローは、Reactの基本的な考え方です。つまり、最上位に単一の情報源があり、そこからUIが派生するというものです。
最新のReact:フックと関数コンポーネント
React 16.8以降、Hooksは関数コンポーネントにおける状態管理と副作用管理の標準的な方法となっています。 クラスコンポーネントが持っていた機能(およびそれ以上の機能)を、クラスを書いたり、 this およびライフサイクル メソッドを明示的に使用すると、 JavaScript モダンを確立する.
関数コンポーネントは、現在Reactのコードベースにおけるデフォルトのスタイルとなっています。 書く代わりに class Example extends React.Component次のような単純な関数を定義します function Example() { return <div />; }状態、副作用、または参照が必要な場合は、次のような関数を介して React に「フック」します。 useState, useEffect and useRefフックはクラス内では使用できません。また、フックのルールに従う必要があります(コンポーネントの最上位レベルで必ず呼び出し、ループや条件の中では絶対に呼び出しないでください)。
その useState Hookは、関数コンポーネントにローカル状態を追加する最も簡単な方法です。 初期値を引数として受け取り、現在の状態値とセッターのペアを返します。配列分割代入のおかげで、通常は次のように記述します。 const = useState(0)React はこの状態を再レンダリング間で保持するため、関数を何度も呼び出しても状態値は記憶されます。
クラスの状態とは異なり、 useState オブジェクトである必要はありません。 数値、文字列、ブール値、配列、オブジェクトなど、データに適したものを格納できます。複数の独立した値が必要な場合は、呼び出すことができます。 useState 数回(例えば、 age, fruit, todosあるいは、単一のオブジェクトを保存し、その中に複数のプロパティを管理することもできますが、更新時には不変性のルールを遵守する必要があります。
が返したセッター関数を呼び出すとき useState同期的に値を変更するのではなく、更新をキューに入れます。 setState 授業中に。 次のレンダリング時に、Reactはコンポーネントに新しい状態値を割り当てます。そのため、同じ同期関数内でセッターを呼び出した直後に状態を読み取っても、古い値が表示されてしまいます。
状態内のオブジェクトとネストされたデータの管理
Reactでは、オブジェクトや配列を含むあらゆるJavaScriptの値をstateに格納できますが、それらは不変のスナップショットとして扱う必要があります。 数値や文字列などのプリミティブ値はそもそも変更できませんが、オブジェクトや配列は技術的には変更可能です。ただし、これらを変更するとReactの前提が崩れ、コンポーネントが更新されないといった微妙なバグが発生する可能性があります。
次のような状態オブジェクトを考えてみましょう。 { x: 0, y: 0 } ポインタの位置を表す。 あなたが書くなら position.x = event.clientX 直接的に、既存のオブジェクトを変更しています。セッターを呼び出していないため、React は値が変更されたことを認識せず、再レンダリングされず、UI はフリーズしたままになります。正しいアプローチは次のとおりです。 setPosition({ x: event.clientX, y: event.clientY })これは、まったく新しいオブジェクトを作成し、React にそれを使用してレンダリングするように指示します。
新しく作成されたオブジェクトの局所的な変異は全く問題ありません。 例えば、新しいオブジェクトを段階的に構築することができます。 const next = { ...prev }; next.city = 'Paris'; 限り next 既にその状態に存在していなかったオブジェクトを変更すると、変更が問題になります。なぜなら、アプリの他の部分がその古い値に依存している可能性があるからです。
オブジェクトの一部のみを更新し、残りの部分はそのままにするには、通常、オブジェクトのスプレッド構文を使用します。 フォーム状態オブジェクトの場合 { firstName, lastName, email }入力変更は次のような方法で処理できます。 setPerson({ ...person, : event.target.value })これは古いプロパティをコピーし、変更されたプロパティのみを上書きします。スプレッドは浅いため、ネストされたオブジェクトにはより注意が必要です。
深くネストされたオブジェクトは、変更するパスの各レベルに沿って新しいコピーを作成する必要があるため、すぐに冗長な更新コードにつながる可能性があります。 たとえば、 person.artwork.city 変更する、あなたは setPerson({ ...person, artwork: { ...person.artwork, city: 'London' } })内部的には「ネストされたオブジェクト」は存在せず、互いに参照し合う独立したオブジェクトが存在するため、複数の親オブジェクトが同じ子オブジェクトを参照している場合、その子オブジェクトを変更すると、複数の場所で同時にデータが変更されることになります。
入れ子構造のスプレッドを頻繁に作成してしまう場合は、状態の形状をフラット化するか、Immerのようなヘルパーライブラリを使用することを検討してみてください。 Immer を使用すると、ミュータブルに見えるコード ( draft.artwork.city = 'London')一方、舞台裏では新しい不変のコピーが生成されます。React では、Immer を Hooks と組み合わせるには、 useImmer use-immer パッケージ。
実践例:フォーム、タイマー、ユーザー入力
実際のアプリケーションでは、カウンターのためだけに状態を管理することはほとんどなく、ユーザー入力、API応答、そして読み込み中、エラー、成功といったUIの「モード」を管理します。 Reactにおける重要な考え方の転換点は、「DOMを操作する」(例えば、「このボタンを無効にする」)のではなく、各状態におけるUIの外観を記述し、その状態を更新するという点です。
例えば、クイズやフォームコンポーネントは、 status 状態を切り替える 'typing', 'submitting' and 'success'. JSXは、送信中は条件付きで送信ボタンを無効にし、正解すると成功メッセージを表示します。命令型のDOMメソッドを呼び出す必要は一切ありません。Reactは新しい状態に基づいて再レンダリングを行い、視覚的な出力が変更されます。
フォーム フィールドの処理は、多くの開発者がクラス ステートのマージとフォーム フィールドの処理の違いに初めて直面する場所です。 useState 行動。 授業で、 setState 渡されたオブジェクトを既存の状態オブジェクトにマージするため、1 つのフィールドを更新しても他のフィールドは削除されません。 useState更新では値全体が置き換えられます。状態がオブジェクトで、呼び出す場合 setState({ email: '...' })、その他のプロパティ(例: password手動でマージしない限り、消えてしまいます。
この違いが、複数のプリミティブな状態変数から単一のオブジェクトにリファクタリングする際に、人々をつまずかせる原因となる。 から変更した場合 const and const 〜へ const そして、一般的な setForm({ : value })そうすると、常にフィールドが1つしかない状態オブジェクトになってしまいます。解決策は、前のオブジェクトを展開することです。 setForm({ ...form, : value }).
より複雑なアプリでは、呼び出しを行うことはほとんどありません。 setState (または setSomethingあらゆる場所から直接。 ReduxやMobXのようなライブラリを使用して状態を一元化したり、 useReducer コンポーネントレベルのステートマシン用のフックです。このような構成でも、同じ不変性の原則が適用されます。唯一の違いは、更新が実行される場所と方法です。
再レンダリング、パフォーマンス、およびuseRefを使用するタイミング
Reactでは、状態が更新されるたびに、その状態を所有するコンポーネントと、デフォルトではそのすべての子コンポーネントが再レンダリングされます。 これは意図的な設計です。再レンダリングによって、UIは最新のデータと同期を保つことができます。しかし、これは同時に、安易な状態配置によって不要な処理が発生し、特に子コンポーネントが負荷の高い計算を実行したり、大きなリストをレンダリングしたりする場合、UIの動作が遅くなる可能性があることを意味します。
入力フィールドと、スキル一覧を表示する別のコンポーネントを備えたアプリを想像してみてください。 親コンポーネントがユーザーが入力しているテキストとリスト自体の両方を所有している場合、スキルリストが変更されていなくても、キーを押すたびにツリー全体が再レンダリングされてしまいます。これは無駄な労力です。
これを最適化する簡単な方法の1つは、子コンポーネントをラップすることです。 React.memo. React.memo は、関数コンポーネントの結果をメモ化する高階コンポーネントです。レンダリング間でそのプロパティが同じであれば、React は再レンダリングをスキップします。したがって、スキルリストコンポーネントは、一度 でラップされると、 React.memo、キーストロークごとに再レンダリングされることはありません。 skills プロパティは実際に変化します(たとえば、新しいスキルを追加したときなど)。
すべての「状態のような」データが useState; 時々 useRef より良いツールです。 その useRef Hook は可変オブジェクトを提供します current コンポーネントのライフサイクル中ずっと保持されるプロパティですが、更新すると 再レンダリングをトリガーします。そのため、タイマーID、DOM要素参照、追跡したいがUIに表示する必要のないカウンターなどを保存するのに最適です。
簡単な例としては、カウンターを実装したものが挙げられます。 useRef useState. カウントを保存する場合 countRef.current イベントハンドラ内でインクリメントすると、内部値は変化しますが、Reactが再レンダリングしないため、表示されるJSXは更新されません。これが重要な違いを示しています。 useState UIを制御する値のためのものです。 useRef これは、レンダリングに影響を与えずに保持しておきたい値を指定するためのものです。
不変性と、なぜ直接変異が落とし穴となるのか
Reactの基本原則の一つは、状態の更新は不変でなければならないということである。 それは、何も変更できないという意味ではありません。既存の値(特にオブジェクトや配列)を変更する代わりに、新しい値を作成し、古い値はUIの履歴スナップショットとして残しておくという意味です。
状態を直接変更すると、あなたの思考モデルとReactが実際に行っていることとの間のつながりが断たれてしまいます。 もしあなたが次のようなことをしたら state.count++ または、状態配列に直接プッシュした場合、更新関数が呼び出されていないため、React は何も変更されたことを認識しません。React が再レンダリングのタイミングを決定するために使用する内部スナップショットは変更されないままですが、コードでは値が変更されたと認識します。これが、リロードすると「自然に修正される」バグが発生する原因です。
また、状態値を別の変数に代入してから、その変数を変更することも避ける必要があります。 例えば、 const newCount = count; newCount++; プリミティブの場合は依然として同じ基底値を変更し、オブジェクトの場合は、 const copy = stateObj; コピーは全く作成されず、同じオブジェクトへの別の参照を作成するだけです。適切なコピーには、次のようなパターンが必要です。 { ...stateObj } オブジェクトまたは アレイ用。
Redux、MobX(不変性を設定する場合)、Immerといったライブラリは、不変パターンを強制したり簡素化したりするために存在している。 Reactの組み込みフックを使用する場合でも、状態管理ライブラリを使用する場合でも、鉄則は変わりません。Reactが変更を認識して再レンダリングすることを期待するのであれば、既存の状態をその場で変更してはいけません。
非同期更新、バッチ処理、および古い状態
Reactの状態に関する、些細ながらも重要な点の1つは、更新が非同期でスケジュールされ、即座に適用されるわけではないということです。 電話するとき setState またはフックセッター setCountReact は、再レンダリングを将来のある時点で「キューに追加」します。そのため、コードがすぐに更新や再レンダリングを行うためにブロックされることはなく、複数の更新をまとめて実行してパフォーマンスをスムーズに保つことができます。
このスケジューリングモデルでは、同じ同期ブロック内でアップデーターを呼び出した直後に状態を読み取ることはできません。 取得できる値は通常、古いスナップショットです。代わりに、アップデーターは「次回レンダリングするときに、この値(またはこの変換関数)を使用してください」というリクエストとして考えるべきです。
これは、クロージャ内から現在の値に基づいて状態を更新する場合に特に重要です。 setTimeout またはサブスクリプションコールバック。 これらのコールバックは、作成された時点での状態をキャプチャします。 setCount(count + 1) タイムアウト中は、 count あなたが言及している情報は、コールバックが実際に実行される頃には古くなっている可能性があります。
この現象は「停滞状態」または「停滞閉鎖」として知られています。 例えば、ボタンをクリックするとタイムアウトを設定し、1秒後に状態をインクリメントする関数を呼び出すボタンがある場合、連続して素早くクリックすると状態が正しくインクリメントされない可能性があります。各タイムアウトコールバックは古い count タイムアウトが予定されていた時刻を捉えた。
確実な解決策は、状態設定関数の関数型更新関数形式を使用することです。 の代わりに setCount(count + 1) タイムアウトの中で、あなたは次のように書きます。 setCount(prevCount => prevCount + 1)これにより、各コールバックは、タイムアウトが作成された時点でスコープ内にあった値ではなく、更新が適用された時点での最新の以前の値を受け取るようになります。これにより、クロージャの動作を変更することなく、古い状態の問題を解消できます。
React のドキュメントでは、あまり知られていない詳細も指摘されています。関数アップデーターが何も返さない場合 (undefined) React は再レンダリングをスキップします。 つまり、更新を明示的に阻止したい場合を除き、更新関数は常に次の状態値を返す(または前の状態値を再利用する)必要があります。これは、標準ではめったに望ましくないことです。 useState 使用法。
非同期スケジューリング、バッチ処理、およびクロージャ動作の組み合わせを理解することは、タイムアウト、間隔、サブスクリプション、または高速なユーザー操作を扱うアプリケーションで信頼性の高い状態ロジックを記述するために不可欠です。 状態設定関数が更新を即座に実行するのではなく、スケジュールに基づいて実行するということを理解すれば、これまでランダムに感じられたバグも納得できるようになるでしょう。
これらのアイデアをすべてまとめると、props 対 state、クラスライフサイクル対 Hooks、不変性、制御コンポーネント、 useRef 非視覚的な値、メモ化、非同期更新、古いクロージャなどを考慮すると、React UI が時間の経過とともにどのように進化するかについての強力で予測可能なモデルが得られます。 命令型のDOM変更という観点から考えるのではなく、明確な状態モデルを設計し、再レンダリングはReactに任せることで、アプリケーションの成長に伴ってコンポーネントの理解、テスト、拡張が容易になります。

