React の useState まとめ

React の state を扱うフックである useState についてまとめました。

この記事は以下の構成になっています。

  • useState とは
  • state とは
  • useState の使い方
  • state と再レンダリング
  • useState の使用例
  • useState を使わないとどうなる?
  • 注意点
    • useState はコンポーネントの直下以外で宣言するとエラーになる
    • set 関数を実行した時点では state は変更されない
    • 同じ state を更新する set 関数を複数実行しても 1 度しか更新されない
    • オブジェクトや配列の state は直接更新してはいけない
    • state を持つコンポーネントの数は最小限にする

useState とは

useState は関数コンポーネントで state を扱うための hooks。

hooks とは関数コンポーネントでロジックを分離するための機能。

state とは

state とは個々のコンポーネント自身が持つ状態、データのこと。

state にはフォームに入力された値や、API から取得したデータ、ローディングや認証情報などが格納される。

useState の使い方

ボタンをクリックするとカウントが 1 増えるカウンターを実装しながら useState の使い方を確認する。

useState は以下の流れで使う。

  1. インポートする
  2. useState に初期値を渡して、stateset〇〇を取得する
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 に初期値を渡して、stateset〇〇を取得する

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 関数が実行された時の一連の流れは以下の通り。

  1. set 関数が実行される
  2. state の変更が予約される
  3. set 関数を実行した時の他の処理が終わった後に、コンポーネントの再レンダリングが行われる
  4. state が変更される
  5. 仮想 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イベントハンドラであるsetTexte.target.valueを渡して state を更新する。

フォームにあらかじめ何かしらのテキストが入力された状態にしたい場合は、useState の引数に初期値を渡して、inputvalue属性に 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 として親から子に渡しておくことで、以下の流れで更新を行う。

  1. 親から子に props として更新関数を渡しておく
  2. 子で関数を実行すると親に通知される
  3. 親で関数を実行して実際に更新される

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} />
    </>

  )
}

処理の流れは以下のようになる。

  1. フォームを入力する
  2. Input コンポーネントonChangeが発生する
  3. Parent コンポーネントから渡されているsetTextイベントハンドラとして実行され、Parent コンポーネントtextの変更を通知する
  4. Parent コンポーネントで実際にsetTextが実行され、textが更新される
  5. 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>
    </>
  )
}

ここではcountletを使って再代入可能な変数で管理している。

この状態でボタンをクリックして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.logcountを確認している。

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 を使うことで、データの流れがわかりやすく、予測しやすいコードになる。