Stateとは?
OZくん、今日はReactのStateについて学んでいきましょう!超重要です!!
State??何ですか?なんとなく重要そうだけど…。
Reactでは、コンポーネントが画面に表示される内容を管理してるんですが、このStateは、コンポーネントの中で変化するデータを保持するために必要なんです!
なるほど…例えば、入力欄に文字を打ったときに表示が変わるような状況で、Stateは必要なんですか?
その通り!!Stateがなければ、入力した文字がReactに伝わらず、画面に反映されないんです。ちょっと簡単な例で説明してみますね!
const Example = () => {
let displayVal; // 表示用の変数を用意
return (
<input
type="text"
onChange={(e) => {
displayVal = e.target.value; // 入力された値をdisplayValに代入
console.log(displayVal); // 値をコンソールに表示して確認
}}
/>
);
};
export default Example;
あれ?このコードではdisplayVal
に入力した値を入れてるけど、これだけだと画面が変わらないんですね?
そうなんです。Reactが画面の再レンダリングをするためには、Stateを使ってdisplayVal
を管理する必要があるんです。さっきのコードでは、displayVal
が変更されてもReactにはその情報が伝わらないんです。
じゃあ、どうやってReactに伝えるんですか?
それが次のテーマであるuseState関数なんです。useState
を使うと、Reactに状態を管理させることができるんです。
状態を管理…?
簡単に言うと、useState
を使えば、値が変わったときにReactが自動で画面を更新してくれるんです。使うためには、先程のコードを、以下のように書き換えられます。
import { useState } from 'react';
const Example = () => {
const [displayVal, setDisplayVal] = useState(''); // 初期値は空文字
return (
<input
type="text"
onChange={(e) => {
setDisplayVal(e.target.value); // 値を更新関数で変更
}}
value={displayVal} // 表示される値もdisplayValに依存
/>
);
};
export default Example;
おお!useState
の中にあるsetDisplayVal
を使うと、自動でReactが画面を再表示してくれるんですね!!
そうです!これで、ユーザーが入力した内容がリアルタイムで画面に反映されるようになるんです。useState
関数の仕組みについては、次に詳しく見てみましょう!!
useStateの役割と使い方
- 接続 (Hook into)
useState
を使うと、React内部でその状態が管理されるようになります
これを「Hook into」と呼んでいます
- 現在の値と更新関数の返却
useState
は「現在の値」と「更新関数」の2つを返してくれます。さっきのコードでいうと、displayVal
が現在の値、setDisplayVal
が更新関数です
- 再レンダリングのトリガー
- 更新関数
setDisplayVal
を使って値を変えると、Reactが自動的にその変更を画面に反映してくれています
- 更新関数
なるほど!useState
を使えば、Reactが自動的に画面を更新してくれるから、値を保持するのも簡単だし、ユーザーの操作に合わせて反映させるのも楽になるんですね!今から帰って実際に使ってみます!!ありがとうございました!
Stateの仕組みをイラストでイメージ
useStateの使い方と注意点
数日後、、、
プロ太先生、あれから、ReactでuseStateを使ってみてるんですけど、エラーが出たり、思った通りに動かなかったりして困ってます。何か使い方にコツがあるんですか?
useStateにはいくつか気をつけるポイントがあります。それを知っておくと、Reactの状態管理がもっと分かりやすくなりますよ!説明しますね。
1.コンポーネントの中で呼び出す
まず、useStateは必ずコンポーネントの中で使わないといけないんです。
コンポーネントの中って、つまり function Example() { ... }
の中ですよね?
他の場所では使えないんですか?
使えません。useStateはReactのフック(Hooks)の一つなんですが、Reactのフックは、Reactコンポーネントの中でのみ動作するように作られているんです。もしコンポーネントの外や、ループや条件分岐の中で呼び出してしまうとエラーになるんですよ。
なんでエラーが出るんですか?
ReactはuseStateを使うコンポーネントを再レンダリングするたびに、同じ順番でフックが呼ばれるように管理しているんだ。もしif文の中でuseStateを呼ぶと、条件によって呼ばれる順番が変わる可能性があるから、エラーになってしまうんです。
なるほど、だからコンポーネントのトップレベルで使うのが基本なんですね!
値の更新と再レンダリングは予約(非同期)される
次の注意点はですね、useStateのset関数を使ってstateを更新すると、その変更はすぐには画面に反映されないんです。
えっ?どういうことですか?
Reactはstateの変更を‘予約’するんです。つまり、setCount(count + 1)と書いても、すぐにcountの値が画面に反映されるわけじゃなくて、次の再レンダリングが発生するタイミングで更新されるんです。
へぇ、非同期なんですね!それは、Reactが効率よくレンダリングを管理するための仕組みなんですか?
そうです!Reactはパフォーマンスを重視しているから、無駄に再レンダリングをしないように、必要なタイミングでまとめて画面を更新してくれるんだ。ちょっと、コードで例を見てみましょう!
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("クリック前のcount:", count);
setCount(count + 1);
console.log("クリック後のcount:", count);
};
return (
<div>
<p>現在のカウント: {count}</p>
<button onClick={handleClick}>カウントを増やす</button>
</div>
);
}
export default Counter;
このコードで、<button>
をクリックするとcount
が増えるんですよね?
でも、handleClick
の中のconsole.log
でcount
を表示しているのに、なんでボタンをクリックしてもconsole.log
でcount
が変わらないんですか?
実は、setCount(count + 1)
を呼んでも、即座にcount
の値が更新されるわけじゃないんです。さっきも言った通り、Reactは変更を予約して、次の再レンダリング時(returnで出力する時)に新しい値を反映するんです。
つまり、setCount(count + 1)
を呼んだ直後のconsole.log("クリック後のcount:", count);
ではreturnの前で、まだcount
が変わっていないから、同じ値が表示されるってことなんですね?
その通り!実際に画面の表示が更新されるのは、次のレンダリング(return)が行われるときです。だから、直後のconsole.log
には反映されません。でも、新しいcount
の値は次の再レンダリング(return)で表示されるようになります。これが、Stateの注意点の1つです。
前回のstate値を使用する場合は更新関数に関数を渡す
ところで、状態を更新するときに前の値を使いたい場合もありますよね?
例えば、count
を1ずつ増やしたいときとか。その場合、setCount(count + 1);
と書いても、うまくいかないことがあるんです。
えっ、なんでですか?
さっきも言ったように、stateの更新は非同期的に行われるから、countの現在の値を直接使って更新すると、意図しない動作になることがあります。そんなときは、前の値を使って新しい値を計算するように、関数を渡すといいです。
setCount((prevCount) => prevCount + 1);
なるほど!これなら prevCount
に前の count
の値が入っているから、確実に1ずつ増やせるんですね!
その通り!特にボタンを連打するような状況では、これを使わないと予期しない結果になることが多いので、覚えておくといいですよ。
オブジェクト型のstateを更新する際には新しいオブジェクトを作成する
じゃあ、user というオブジェクトを state として使う例を見てみよう。
const [user, setUser] = useState({ name: "OZ", age: 17 });
ここで、user の name
プロパティだけを更新したい場合、 user.name = '新しい名前'
と直接書き換えたくなるかもしれませんよね。でも、これはやっちゃいけません。
どうしてですか?
React は state が変更されたことを検知して再レンダリングを行う仕組みなんだけど、state のオブジェクトを直接変更すると、React がその変更をうまく認識できないんです。
なるほど…じゃあ、どうすればいいんですか?
そこで、新しいオブジェクトを作成して、それを setUser
関数に渡す必要があります。このとき、...prevUser
というのが出てくるんです。
え、...prevUser
ってどういう意味なんですか?
スプレッド構文 (...
) の意味を詳しく説明
この ...prevUser
はスプレッド構文と呼ばれるもので、オブジェクトの内容を展開する役割があります。たとえば、user
が { name: 'OZ', age: 17 }
というオブジェクトだとすると、...prevUser
と書くと、user
オブジェクトの中身が { name: 'OZ', age: 17 }
として展開されるんだよ。つまり、スプレッド構文を使うと、オブジェクトのすべてのプロパティがコピーされます。実際のコードも見ておきましょう。
setUser((prevUser) => ({ ...prevUser, name: '新しい名前' }));
ここで何が起きているかを分解してみると、
prevUser
:setUser
に渡される関数の引数prevUser
は、現在のuser
の状態を表すオブジェクトです。つまり、{ name: 'OZ', age: 17 }
です。{ ...prevUser }
: スプレッド構文 (...prevUser
) を使ってprevUser
の中身を新しいオブジェクトに展開しています。これにより、新しいオブジェクトが{ name: 'OZ', age: 17 }
として作成されます。name: '新しい名前'
: 新しいオブジェクトに{ name: '新しい名前' }
というプロパティを追加しています。このとき、元々のname: 'OZ'
が上書きされ、新しいname
の値が'新しい名前'
になります。
最終的に、次のようなオブジェクトが setUser
に渡されます。
{ name: '新しい名前', age: 17 }
なるほど、だから setUser
に渡されるのは age
がそのままで、name
だけが変わった新しいオブジェクトなんですね!
そうです!新しいオブジェクトを作成することで、React が state の変更を認識して、必要に応じて再レンダリングを行ってくれるんです。
stateの値はコンポーネントごとに独立して管理される
次に、stateはコンポーネントごとに独立していることも覚えておきましょう。
たとえば、Example
というコンポーネントが2つあるとします。それぞれに count
というstateがあっても、片方の count
を更新してももう片方には影響しません。
なるほど!同じコンポーネントでも別々の状態を持つことができるんですね!
一度消滅したコンポーネントのstateの値はリセットされる
じゃあ、モーダルみたいに、表示を消してもう一度表示した場合、stateはどうなるんですか?
もし条件分岐でコンポーネントを一時的に非表示にして、再度表示させた場合、そのコンポーネントのstateは初期化されてしまいます。
なるほど!モーダルを一度閉じて再度開いたときに、入力内容がリセットされるのもそのためなんですね。
stateをpropsとして渡すことで子コンポーネントで利用可能
親コンポーネントで定義した state
は、そのまま親コンポーネントの中だけで使うこともできるんだけど、props
を使って子コンポーネントにも渡して利用することができますよ。具体的なコードを見てみましょう。
まず、親コンポーネントで count
という state
を定義します。
import { useState } from "react";
import ChildComponent from "./Childcomponent";
function ParentComponent() {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
return (
<div>
<h1>現在のカウント: {count}</h1>
<ChildComponent count={count} />
<button onClick={incrementCount}>ボタン</button>
</div>
);
}
export default ParentComponent;
このコードのポイントは、<ChildComponent count={count} />
という部分です。ここで count={count}
として、count
の state
を props
として子コンポーネントに渡しています。
子コンポーネントで props
を受け取る
次に、子コンポーネント側で count
を使えるようにします。ChildComponent
は次のようになります。
import React from 'react';
function ChildComponent({ count }) {
return (
<div>
<p>子コンポーネントでのカウント: {count}</p>
</div>
);
}
export default ChildComponent;
このコードでは、{ count }
という記述で props
の count
を受け取っています。props
は通常、props.count
としてアクセスできますが、ここでは JavaScript の「分割代入(destructuring)」という書き方を使って、count
だけを直接取り出して使っています。
親から子にデータが流れる仕組み
- 親コンポーネントで
state
を定義:ParentComponent
でcount
をuseState
で管理しています。 props
として子コンポーネントに渡す:ParentComponent
の中で、<ChildComponent count={count} />
として、count
をprops
としてChildComponent
に渡しています。- 子コンポーネントで
props
を受け取って表示:ChildComponent
は、親から渡されたprops
のcount
を受け取って、子コンポーネントでのカウント: {count}
として画面に表示しています。
なるほど!これで、親コンポーネントで管理している count
のデータを、子コンポーネントでも使えるようになるんですね。
そうです!これが、React で state
を共有する基本的な方法です。親から子に props
を通してデータが流れることで、親の state
を変更すれば、子コンポーネントでもその変化が反映されるようになります。
コンポーネントの位置によってはstateが維持される
あと、覚えておいてほしいのは、Reactで特定の条件によってコンポーネントを表示・非表示にするときに、key
属性がないと、再レンダリングされたときにstateがリセットされる場合があることです。
状態を維持するためのコードの書き方
例えば、フォームに入力していた内容が突然消えたり、カウントしていた数がリセットされたりしたら困りますよね?条件付きでコンポーネントを表示・非表示にする場合、key
属性を設定することで、Reactはそのコンポーネントを同一のものとして認識してくれます。これにより、状態が保持されるようになるんです。実際のコードで見てみましょう!
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>カウント:{count}</h2>
<button onClick={() => setCount(count + 1)}>カウントを増やす</button>
</div>
);
}
function App() {
const [showCounter, setShowCounter] = useState(true);
return (
<div>
<button onClick={() => setShowCounter(!showCounter)}>
カウンターを{showCounter ? "隠す" : "表示する"}
</button>
{showCounter && <Counter />}
</div>
);
}
export default App;
コードの説明
Counter
コンポーネント:Counter
はcount
という状態を持ち、ボタンを押すとカウントが増える仕組みになっています。
App
コンポーネント:showCounter
という状態を持ち、showCounter
がtrue
のときだけCounter
コンポーネントを表示します。- 「カウンターを隠す/表示する」ボタンで
showCounter
の値をトグルすることで、Counter
を表示・非表示に切り替えます。
状態がリセットされる問題
これを実行すると、Counter
コンポーネントを隠してもう一度表示すると、カウントがリセットされちゃいますね。
そうです。Counter
コンポーネントを一度非表示にすると、そのコンポーネントがDOMから削除されるので、もう一度表示すると新しいインスタンスとして再生成されてしまい、カウントも初期値に戻ります。
解決策:コード修正
import { useState } from "react";
function Counter({ count, countIncrement }) {
return (
<div>
<h2>カウント:{count}</h2>
<button onClick={countIncrement}>カウントを増やす</button>
</div>
);
}
function App() {
const [count, setCount] = useState(0);
const [showCounter, setShowCounter] = useState(true);
const countIncrement = () => {
setCount(count + 1);
};
return (
<div>
<button onClick={() => setShowCounter(!showCounter)}>
カウンターを{showCounter ? "隠す" : "表示する"}
</button>
{showCounter && <Counter count={count} countIncrement={countIncrement} />}
</div>
);
}
export default App;
主な変更点は以下になります。
- カウントの状態(
count
)をApp
コンポーネントに移動 Counter
コンポーネントを制御コンポーネントに変更し、以下のpropsを受け取るように変更count
: 現在のカウント値onIncrement
: カウントを増やすための関数
おお〜!!カウントが保存されてる!!これは色んな場面で使えそうですね!!
プロ太先生!ありがとうございました!!useStateを使う上で注意すべきポイントがよく分かりました!
どういたしまして!これを意識しながら、実際にコードを書いてみるともっと理解が深まるはずだよ。Reactのstate管理をマスターすれば、もっと便利にアプリが作れるようになるので、ぜひ頑張ってくださいね!
まとめ
今回の記事の主なポイント、
- useStateの基本概念と必要性
- コンポーネント内で変化するデータの管理
- 画面の自動更新との連携
- useStateの重要な特徴
- コンポーネント内でのみ使用可能
- 状態更新は非同期で行われる
- コンポーネントごとに独立した状態管理
- 実践的な使用方法
- 前回のstate値を使用する場合の正しい更新方法
- オブジェクト型のstate更新時の注意点
- propsを使った親子コンポーネント間のstate共有
- よくある課題と解決策
- コンポーネントの表示/非表示時のstate維持方法
- 親コンポーネントでのstate管理による解決策
- 状態を保持するためのコードの書き方の例