今回はReactの基礎として大変重要な概念であるstateについて学習していきます。
入力した値を画面に表示したい
前回学んだonChangeを使って、入力欄の値をそのまま画面に表示する仕組みを作ってみましょう。
まず思いつくのは「値が変わったタイミングで変数に格納し、その変数を画面に出す」という方法だと思います。実際に書いてみます。
//Example.tsx
import "./Example.css"
const Example = ()=> {
let displayValue = "";
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>)=> {
displayValue = e.target.value;
}
return (
<div className="component">
<input type="text" onChange={changeHandler} />
<h3>{displayValue}</h3>
</div>
);
}
export default Example;
一見うまくいきそうですが、入力欄に文字を打っても画面には何も表示されません。
なぜ画面が変わらないのか
理由は2つあります。
1つ目は、コールバック関数の中しか再実行されないからです。
onChangeで実行されるのは、あくまでchangeHandlerの中だけです。returnに続くTSXの部分は再実行されません。画面の表示を作り直すには、Exampleという関数自体を再実行する必要があります。
代入自体は行われているのに、画面を作る部分が動いていない、という状態です。
2つ目は、再実行されると変数が初期化されてしまうからです。
仮にExampleが再実行されたとしても、let displayValue = "";という変数宣言が再び呼ばれるので、値は空に戻ってしまいます。いくら代入しても、画面上に表示されるのは空のままです。
つまり画面を変えるには、次の2つが必要だということが分かります。
- Reactにコンポーネントを再実行してもらい、新しいReact要素を作ってもらう
- 変更した値をどこかに保持しておく
この1つ目を再レンダリング、2つ目の保持しておく場所を**state(状態)**と呼びます。どちらも重要なキーワードなので覚えておいてください。
これらを解決するのがuseStateという関数です。
useStateを使ってみる
まずはReactのライブラリからuseStateをインポートします。
//Example.tsx
import { useState } from "react"
import "./Example.css"
const Example = ()=> {
const valueArray = useState("");
console.log(valueArray);
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>)=> {
}
return (
<div className="component">
<input type="text" onChange={changeHandler} />
</div>
);
}
export default Example;
useStateの引数には初期値を渡します。試しにuseState(0)としてコンソールを見ると、戻り値は配列になっていることが分かります。
- 配列の0番目 … 現在の値(初期値に渡した
0が入っている) - 配列の1番目 … 値を更新するための関数
この2つを使って書くとこうなります。
//Example.tsx
import { useState } from "react"
import "./Example.css"
const Example = ()=> {
const valueArray = useState("");
const setFunction = valueArray[1];
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>)=> {
setFunction(e.target.value);
}
return (
<div className="component">
<input type="text" onChange={changeHandler} />
<h3>{valueArray[0]}</h3>
</div>
);
}
export default Example;
入力欄に111と打つと、画面にも111と表示されるようになりました。
分割代入で書く
添字で指定するのは読みにくいので、実際には分割代入で受け取ります。
//Example.tsx
import { useState } from "react"
import "./Example.css"
const Example = ()=> {
const [value, setValue] = useState("");
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>)=> {
setValue(e.target.value);
}
return (
<div className="component">
<input type="text" onChange={changeHandler} />
<h3>{value}</h3>
</div>
);
}
export default Example;
取得用の値と更新用の関数を、それぞれvalue・setValueという名前で受け取っています。setXxxという名前にするのが慣習です。
本当に再実行されているのか確認する
コンポーネントの先頭にconsole.logを置いて確かめてみましょう。
const Example = ()=> {
const [value, setValue] = useState("");
console.log("再レンダリングされました");
...
}
useStateを使わなかったときは、画面の初期表示のとき1回しか出力されませんでした。useStateを使うと、入力するたびに再レンダリングされましたと出力されます。
つまりsetValueを呼んだタイミングで、関数コンポーネント自体が再実行されているということです。これは大変重要な概念なので覚えておいてください。
useStateが行っていること
useStateが何をしているのか、順を追って整理します。
- 実行されたタイミングでReact内部と接続し、「このコンポーネントには状態の管理が必要ですよ」と知らせる
- 初期値が現在の値として保持され、ペアになる更新用関数も準備される
- その結果として
[値, 更新用関数]という配列が返される - 更新用関数に値を渡すと、React内部の値が入れ替わり、あわせてコンポーネントを再実行してくださいという指示が送られる
更新用関数が「値の入れ替え」と「再実行の依頼」の2つの役割を持っている、というのがポイントです。
このReact内部で保持管理される値のことをstateと呼びます。状態とも呼ばれ、その管理を状態管理と言います。
useStateはReactフックと呼ばれる関数の1つです。フックには「何かに接続する」という意味があり、React内部で値を保持する仕組みと接続する、というイメージです。
stateはコンポーネントごとに保持管理されます。レンダリングをまたいで値を保持しておきたいときは、stateに保存する、と覚えておいてください。
useStateの呼び出しルール
useStateはコンポーネントのトップレベルでしか呼べません。
// これはエラー
const [count, setCount] = useState(0);
const Example = ()=> {
...
}
// これもエラー
const Example = ()=> {
if (flag) {
const [count, setCount] = useState(0);
}
...
}
こう書くと、次のエラーが出ます。
React Hook "useState" is called conditionally.
Hooks must be called in the exact same order in every component render.
if文だけでなく、for文やwhile文の中も同様にエラーになります。
理由は、Reactの内部でstateが呼ばれた順番で管理されているからです。1番目に宣言したstate、2番目に宣言したstate、という形で保持されているので、条件によって呼ばれたり呼ばれなかったりすると、順番が崩れて別の値を取ってきてしまいます。
そのため、useStateは必ずコンポーネントのトップレベルで、毎回同じ順番で呼ぶというルールになっています。
更新用関数はすぐには反映されない
もう1つ重要な性質があります。カウンターを作って確かめてみましょう。
//Example.tsx
import { useState } from "react"
import "./Example.css"
const Example = ()=> {
const [count, setCount] = useState(0);
const countUp = ()=> {
setCount(count + 1);
console.log(count);
}
return (
<div className="component">
<p>カウント:{count}</p>
<button onClick={countUp}>+</button>
</div>
);
}
export default Example;
初期値は0です。ボタンを押すとsetCount(count + 1)で1が設定されるので、直後のconsole.log(count)も1になりそうなものですが、実際には0が出力されます。
更新用関数は「React内部の値を変えてください」という指示を出すだけで、その場では反映されません。次のレンダリングのときの値にする、という予約をしているだけです。これを専門的には非同期で処理されると表現します。
そのため、こう書いても結果は1にしかなりません。
const countUp = ()=> {
setCount(count + 1);
setCount(count + 1);
}
この時点のcountはどちらも0なので、「stateを1にしてください」という指示を2回出しているだけになるからです。
前の値を使いたいときは関数を渡す
現在の値をもとに更新したい場合は、更新用関数に関数を渡します。
const countUp = ()=> {
setCount((prevState)=> prevState + 1);
setCount((prevState)=> prevState + 1);
}
こう書くと、前回設定された値が引数に渡ってきて、その戻り値が次のstateの値になります。今度はボタンを1回押すと2まで増えます。
stateを変更するときは現在の値を見て処理する場合が多いので、この書き方はぜひ覚えておいてください。
確認問題
以下のExample.tsxは、ボタンを押しても画面のカウントが増えません。
//Example.tsx
import "./Example.css"
const Example = ()=> {
let count = 0;
const countUp = ()=> {
count = count + 1;
console.log(count);
}
return (
<div className="component">
<p>カウント:{count}</p>
<button onClick={countUp}>+</button>
</div>
);
}
export default Example;
問題
- コンソールには数字が増えて表示されるのに、画面の表示が変わらない理由を2つ挙げてください
-
useStateを使って正しく動くように書き換えてください - ボタンを1回押したときに2ずつ増えるようにするには、どう書けばよいか答えてください
(下に解答を記載しています)
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
解答
1. 理由
-
onClickで実行されるのはcountUpの中だけで、returnに続くTSXは再実行されないから - 仮に
Exampleが再実行されても、let count = 0;で初期化されてしまうから
2. useStateで書き換える
//Example.tsx
import { useState } from "react"
import "./Example.css"
const Example = ()=> {
const [count, setCount] = useState(0);
const countUp = ()=> {
setCount(count + 1);
}
return (
<div className="component">
<p>カウント:{count}</p>
<button onClick={countUp}>+</button>
</div>
);
}
export default Example;
3. 1回で2ずつ増やす
const countUp = ()=> {
setCount((prevState)=> prevState + 1);
setCount((prevState)=> prevState + 1);
}
setCount(count + 1)を2回書いても1にしかなりません。前回の値を受け取る関数を渡すことで、2回分が正しく反映されます。
チェックリスト
- 変数に代入しても画面が変わらない理由を2つ説明できる
- 再レンダリングとstateという2つのキーワードの意味が分かる
-
useStateの戻り値が[値, 更新用関数]の配列だと理解している -
分割代入で受け取り、更新用関数は
setXxxという名前にしている - 更新用関数を呼ぶとコンポーネントが再実行されると知っている
-
useStateはコンポーネントのトップレベルでのみ呼んでいる - if文やfor文の中で呼ぶとエラーになる理由を説明できる
- 更新用関数の結果はすぐには反映されない(非同期)と理解している
- 前の値を使いたいときは更新用関数に関数を渡している
ステート編②はここまで。
次回はstateの扱い方について学習していきます!