React の state を扱うフックである useState についてまとめました。
この記事は以下の構成になっています。
- useState とは
- state とは
- useState の使い方
- state と再レンダリング
- useState の使用例
- useState を使わないとどうなる?
- 注意点
useState とは
useState は関数コンポーネントで state を扱うための hooks。
hooks とは関数コンポーネントでロジックを分離するための機能。
state とは
state とは個々のコンポーネント自身が持つ状態、データのこと。
state にはフォームに入力された値や、API から取得したデータ、ローディングや認証情報などが格納される。
useState の使い方
ボタンをクリックするとカウントが 1 増えるカウンターを実装しながら useState の使い方を確認する。
useState は以下の流れで使う。
- インポートする
- useState に初期値を渡して、
state
とset〇〇
を取得する
import { useState } from 'react' export const Counter = () => { const [count, setCount] = useState(0) const countUp = () => { setCount(count + 1) } return ( <> <div>{count}</div> <button onClick={countUp}>+</button> </> ) }
1. インポートする
まず useState はreact
からインポートする必要がある。
import { useState } from 'react'
2. useState に初期値を渡して、state
とset〇〇
を取得する
const [count, setCount] = useState(0)
useState は引数に初期値を受け取り、現在の state とその state を更新するためのset〇〇
関数を分割代入で返す。
例では、初期値に 0 を渡し、現在の state のcount
と、count
を更新する関数としてsetCount
を受け取る。
これで state を作成できたので、あとは JSX で state を表示したり、set 関数を使って state を変更したりできる。
例のコードでは、setCount
を実行するcountUp
関数を実装して、ボタンがクリックされた時にcountUp
が実行されてカウントがプラス 1 されるようにしている。
set 関数には値を指定すると、その値に更新される。値ではなく関数を渡す方法もあるが、それについては 後述の prevState を使うで解説。
useState の内部
先ほどの useState をconsole.log
で確認すると以下のようになる。
(2)[0, ƒ] 0: 0 1: ƒ () length: 2 [[Prototype]]: Array(0)
ログで表示されるように、useState は値と関数の 2 つの要素をもつ配列を返している。
この値が state で、関数が state を更新するための set 関数。
配列なのでインデックスで要素にアクセスできるが、分割代入を使って取得するのが一般的。
const [count, setCount] = useState(0)
state と再レンダリング
set 関数が実行されるとコンポーネントが再レンダリングされる。
再レンダリングとは、簡潔に言えば状態の変化に伴ってコンポーネントが更新されること。
再レンダリングは state か props が更新された時か、親コンポーネントが再レンダリングされた時に起こる。
set 関数を実行した時にすぐに state が変更されるわけではなく、set 関数は state の変更を予約して、再レンダリングが行われるタイミングで state が変更される。
set 関数が実行された時の一連の流れは以下の通り。
- set 関数が実行される
- state の変更が予約される
- set 関数を実行した時の他の処理が終わった後に、コンポーネントの再レンダリングが行われる
- state が変更される
- 仮想 DOM の変更がリアル DOM に反映され、ブラウザの表示が更新される
useState の使用例
フォームの入力を扱う
React でフォームを扱うときは入力された値を state で管理するので、その例を確認する。
以下は input タグにテキストを入力するだけのコンポーネント。
import { useState } from 'react' export const Input = () => { const [text, setText] = useState('') return ( <input type='text' value={text} onChange={(e) => setText(e.target.value)} /> ) }
テキストの入力時はonChange
を使う。onChange
はフォームのvalue
属性が変更された時に呼び出されるイベント。イベントが呼び出された時にコールバック関数が実行される。
フォームに入力された値はe
というイベントオブジェクトを使ったe.target.value
で受け取る。そしてonChange
のイベントハンドラであるsetText
にe.target.value
を渡して state を更新する。
フォームにあらかじめ何かしらのテキストが入力された状態にしたい場合は、useState の引数に初期値を渡して、input
のvalue
属性に state を渡す。
子コンポーネントで親の state を使う
子コンポーネントで親の state を使いたいときは、親から子に props として渡すだけ。
Parent.jsx
import { useState } from 'react' import { Child } from './Child' export const Parent = () => { const [name, setName] = useState('子コンポーネント') return ( <> <h2>親コンポーネント</h2> <Child name={name} /> </> ) }
Child.jsx
export const Child = ({ name }) => { return <>{name}</> }
子コンポーネントから親の state を更新する
先ほどの state を props として渡す場合、props は読み取り専用なのでそのまま更新してはいけない。そこで、更新関数も props として親から子に渡しておくことで、以下の流れで更新を行う。
- 親から子に props として更新関数を渡しておく
- 子で関数を実行すると親に通知される
- 親で関数を実行して実際に更新される
Parent.jsx
import { useState } from 'react' import { Child } from './Child' export const Parent = () => { const [name, setName] = useState('子コンポーネント') const handleClick = () => { setName('変更されました') } return ( <> <h2>親コンポーネント</h2> <Child name={name} onClick={handleClick} /> </> ) }
Child.jsx
export const Child = ({ name, onClick }) => { return ( <> {name} <button onClick={onClick}>名前変更</button> </> ) }
prevState を使う
set 関数に値を直接渡すのではなく、以下のように関数を渡すと、前の状態をもとに新しい状態を計算できる。
setCount((prevCount) => prevCount + 1)
このやり方では、常に最新の状態に基づいて新しい状態を計算でき、他の処理で状態が変更される場合でも意図しない結果を防ぐことができる。
state を共有する
先ほどのテキストフォームで入力した値を、入力と同時に別のテキストとして表示する、といったような処理について考える。
この場合は別のコンポーネントで state を共有しなければならないので、フォームのコンポーネントとテキストコンポーネントの共通の親コンポーネントを作成して、この親コンポーネントで state を保持する。
Input.jsx
export const Input = ({text, setText}) => { return ( <input type='text' value={text} onChange={(e) => setText(e.target.value)} /> ) }
Text.jsx
export const Text = ({text}) => { return ( <p>{text}</p> ) }
Parent.jsx
import { useState } from 'react' import { Input } from './Input' import { Text } from './Text' export const Parent = () => { const [text, setText] = useState('') return ( <> <Input text={text} setText={setText}> <Text text={text} /> </> ) }
処理の流れは以下のようになる。
- フォームを入力する
- Input コンポーネントで
onChange
が発生する - Parent コンポーネントから渡されている
setText
をイベントハンドラとして実行され、Parent コンポーネントにtext
の変更を通知する - Parent コンポーネントで実際に
setText
が実行され、text
が更新される - Parent コンポーネントが更新された
text
を Input と Text に渡す =text
が共有される
このように親コンポーネントで state を管理し、子コンポーネントに渡すことで state を共有できる。この方法を state のリフトアップという。
ブログアプリのプレビュー機能とかに使える。
useState を使わないとどうなる?
例えば以下のように、useState を使わずにコンポーネントの状態を実装しようとしたとする。
export const Counter = () => { let count = 0 const countUp = () => { count++ console.log(count) } return ( <> <div>{count}</div> <button onClick={countUp}>+</button> </> ) }
ここではcount
をlet
を使って再代入可能な変数で管理している。
この状態でボタンをクリックしてcountUp
を実行してもcount
の値はコンソール上で変化していることが確認できるが、画面の表示は 0 のままである。
これは値は更新されているが、再レンダリングが発生していないことが原因である。
再レンダリングを発生させるためには更新関数を使わなければならないので、state を扱うためには useState を使う必要がある。
state の注意点
state を使う上で以下の注意点がある。
- useState はコンポーネントの直下以外で宣言するとエラーになる
- set 関数を実行した時点では state は変更されない
- 同じ state を更新する set 関数を複数実行しても 1 度しか更新されない
- オブジェクトや配列の state は直接更新してはいけない
- 1 つのコンポーネントが保持する state はなるべく少なくする
useState はコンポーネントの直下以外で宣言するとエラーになる
useState は if 文の中や JSX 内で宣言するとエラーになるので、コンポーネントの直下で宣言する。useState のルールというより hooks の決まり事。
これは React がコンポーネントが再レンダリングされるたびに同じ順番で hooks を呼び出すことで、その hooks が何に対応しているか追跡しているからである。
仮に条件分岐の中で hooks を呼び出すと、その hooks が呼び出されなかったり、呼び出す順番が変わってしまうことがあるため、React が期待する動作が保証されなくなる。
set 関数を実行した時点では state は変更されない
再レンダリングのところでも触れたが、set 関数を実行した時点では state は変更されない。
例を挙げると、以下のコードではcountUp
の中でsetCount
を実行する前後で、console.log
でcount
を確認している。
import { useState } from 'react' export const Counter = () => { const [count, setCount] = useState(0) const countUp = () => { console.log(count) // 0 setCount((prev) => prev + 1) console.log(count) // 0 } return ( <> <h3>{count}</h3> <button onClick={countUp}>+</button> </> ) }
setCount
の直前では初期値である 0 が表示される。
setCount
の後のconsole.log
ではsetCount
が行われた後なので、0 にプラス 1 した 1 が表示されると思いきや、こちらも 0 が表示される。
このように set 関数を実行したらすぐに state が変更されるのではなく、state の変更を予約して、再レンダリングが行われる過程で state は変更される。
同じ state を更新する set 関数を複数実行しても 1 度しか更新されない
先程の state の変更が予約されるという挙動から、同じ state を更新する set 関数を複数実行したとしても、その時点での state の更新の予約がされるだけなので、state は 1 度しか変更されない。
import { useState } from 'react' export const Counter = () => { const [count, setCount] = useState(0) const countUp = () => { setCount(count + 1) setCount(count + 1) setCount(count + 1) } return ( <> <h3>{count}</h3> <button onClick={countUp}>+</button> </> ) }
上記のコードではsetCount
を 3 回実行しているのでプラス 3 されると思いきや、1 しかプラスされない。
オブジェクトや配列の state は直接更新してはいけない
オブジェクトや配列の state は直接更新せずに、以下のようにスプレッド構文を使って新しいオブジェクトや配列を作って更新を行うのが一般的。
import React, { useState } from 'react' const User = () => { const [user, setUser] = useState({ name: 'Alice', age: 25 }) const updateName = () => { setUser((prevUser) => ({ ...prevUser, name: 'Bob', })) } return ( <div> <h1>{user.name}</h1> <h2>{user.age}</h2> <button onClick={updateName}>名前を変更</button> </div> ) }
これを理解するには、プリミティブ型の値はイミュータブルな値であり、オブジェクトや配列はミュータブルな値であることを理解する必要がある。
イミュータブルというのは、作成後にその内容を変更できないデータのこと。プリミティブ型の値は値そのものが保持されるため、後から変更することはできない。
一方、ミュータブルというのは、作成後にその内容を変更できるデータのこと。オブジェクトや配列は値そのものではなく、参照を保持するので、変更が行われた場合、同じ参照を保持する他の変数にも影響がある。
これらの理由から、オブジェクトや配列を直接変更すると同じデータを参照している他のコードに影響を及ぼす可能性があり、挙動が予測しづらくなるため、スプレッド構文を使って新しくオブジェクトや配列を作成してそれを変更する。
state を持つコンポーネントの数は最小限にする
state が増えると、状態の依存関係が複雑になり、影響範囲が予測しづらく、不要な再レンダリングが発生する可能性もあるため、できるだけ state を持つコンポーネントの数は最小限にすべき。
useState や state の使用についてというよりは、アプリ全体の設計や実装における注意点。
1 つのコンポーネントが保持する state はなるべく少なくする
アプリ全体で扱う state はなるべく最小限にすると同時に、1 つのコンポーネントが保持する state もできるだけ少なくすべき。
基本的に props を使い、必要な場合のみ state を使うことで、データの流れがわかりやすく、予測しやすいコードになる。