デザインについての学習メモブログ

React入門 #07 – useStateで状態管理の基礎をマスター

記事内に広告が含まれています。

React入門 #07 – useStateで状態管理の基礎をマスター

これまで学んだpropsは、親コンポーネントから子コンポーネントへ一方向にデータを渡すものでした。

しかし、ユーザーの操作によってアプリのデータが変わる場合、どうすればよいでしょう?

その答えが状態(state)です。

この記事では、React Hooksの中で最も重要なuseStateを詳しく学んでいきます。

状態管理の基礎を理解

状態(state)とは?

💡 このセクションで学ぶこと

状態(state)の基本概念と、propsとの違いを理解します。

Reactアプリケーションにおいて、なぜ状態管理が必要なのかどんな場面で使うのかが明確になります。

状態の概念

状態は、時間とともに変わる可能性のあるコンポーネント内のデータです。

propsと異なり、コンポーネント内で管理され、変更することができます。

例:

  • カウンターの数値:ボタンをクリックするたびに増える「いいね」の数
  • フォーム入力の値:ユーザーが入力している検索ボックスのテキスト
  • トグルスイッチのOn/Off:ダークモードの有効/無効状態
  • リストの表示/非表示:ドロップダウンメニューの開閉状態
  • ユーザーの選択状態:チェックボックスの選択/非選択

状態とpropsの違い

状態(state)とprops、この2つは似ているようで大きく異なります。

以下の表で違いを確認しましょう。

特性propsstate
更新可能❌ 読み取り専用✅ 変更可能
由来親から渡されるコンポーネント内で定義
変更方法親を通じて変更setStateで直接変更
用途親→子のデータ伝達コンポーネント内のデータ

💡 重要なポイント:

propsは「受け取るだけ」、stateは「管理できる」という違いを覚えておきましょう。

useStateの基本

💡 このセクションで学ぶこと

useStateの基本的な使い方をマスターします。

状態変数更新関数初期値3つの要素の役割を理解し、実際にコードを書けるようになります。

useStateとは?

useStateは、React Hooksの一種で、関数コンポーネントに”状態管理機能”を追加するための機能です。

以前はReact(クラスコンポーネント)では、状態を使うためにクラス構文が必須でしたが、useStateの登場により、シンプルな関数コンポーネントでも状態管理ができるようになりました。

Hookとは、

関数コンポーネントの中で状態管理副作用などの機能を使えるようにするための特別な関数です。

他のHookについては、別の記事で詳しく解説します。

基本的な使い方
  • 1行目でuseStateを使うためにHook(フック)の取得行います。
  • 4行目の定義により、管理したい情報が定義され、状態管理できるようになります。
  • 9行目の更新関数( useXXX() )を使って変数の値を更新します。

JavaScript版:

JSX
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
    </div>
  )
}

TypeScript版:

TSX
import { useState } from 'react'

function Counter(): JSX.Element {
  const [count, setCount] = useState<number>(0)
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
    </div>
  )
}

🖐️ 何で普通の変数で状態管理をしない?

単なる変数では変更してもそれに該当する画面の変更箇所を更新するタイミングがわからない。そのため、該当箇所のDOM操作も合わせてかく必要があり、コードが複雑になる。

useState を使うと、値が変わったことをReactに自動で伝えて、画面を更新してくれる。

useStateの構造

TSX
const [状態変数, 更新関数] = useState(初期値)
各部分の説明:
1. 状態変数(count)

現在の状態値を保持し、読み取り用で直接は値を変更できない

2. 更新関数(setCount)
  • 状態を更新するための関数
  • この関数が呼ばれると、コンポーネントが再レンダリング(再描画)される
  • 命名規則:set + 大文字で始まる状態名
3. 初期値(0)
  • 状態の最初の値
  • コンポーネントが最初にマウントされるときのみ使われる

useStateの実践例

💡 このセクションで学ぶこと:

実際のアプリケーションでよく使われるパターンを3つ習得します。f

「いいねボタン」、「フォームバリデーション」、「ダークモード切り替え」など、すぐに使える実践的なコードを学べます。

基本:カウンター

「いいね」などのカウントに応用できます。

JavaScript版:

JSX
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  
  const increment = () => {
    setCount(count + 1)
  }
  
  const decrement = () => {
    setCount(count - 1)
  }
  
  const reset = () => {
    setCount(0)
  }
  
  return (
    <div className="counter">
      <h1>{count}</h1>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>リセット</button>
    </div>
  )
}

export default Counter

TypeScript版:

TSX
import { type JSX } from 'react'
import { useState } from 'react'

function Counter(): JSX.Element {
  const [count, setCount] = useState<number>(0)
  
  const increment = (): void => {
    setCount(count + 1)
  }
  
  const decrement = (): void => {
    setCount(count - 1)
  }
  
  const reset = (): void => {
    setCount(0)
  }
  
  return (
    <div className="counter">
      <h1>{count}</h1>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>リセット</button>
    </div>
  )
}

export default Counter

フォーム入力の管理

JavaScript版:

JSX
import { useState } from 'react'

function NameForm() {
  const [name, setName] = useState('')
  const [submitted, setSubmitted] = useState(false)
  
  const handleSubmit = (e) => {
    e.preventDefault()
    if (name.trim()) {
      setSubmitted(true)
      setName('')
    }
  }
  
  return (
    <div>
      {submitted && <p>こんにちは、{name}さん!</p>}
      
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="名前を入力"
        />
        <button type="submit">送信</button>
      </form>
    </div>
  )
}

export default NameForm

TypeScript版:

TSX
import { type JSX, useState } from 'react'

function NameForm(): JSX.Element {
  const [name, setName] = useState<string>('')
  const [submitted, setSubmitted] = useState<boolean>(false)
  
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault()
    if (name.trim()) {
      setSubmitted(true)
      setName('')
    }
  }
  
  return (
    <div>
      {submitted && <p>こんにちは、{name}さん!</p>}
      
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="名前を入力"
        />
        <button type="submit">送信</button>
      </form>
    </div>
  )
}

export default NameForm

*1行目は分割代入により2つのimportを1行で行なっています。(カウンターのサンプルでは分けてimportしていました。)ただし、可読性が落ちるので読みやすさに応じて使い分けてください。

トグルスイッチ

JavaScript版:

JSX
import { useState } from 'react'

function Toggle() {
  const [isOn, setIsOn] = useState(false)
  
  const toggle = () => {
    setIsOn(!isOn)
  }
  
  return (
    <div>
      <h2>ライト: {isOn ? 'ON' : 'OFF'}</h2>
      <button 
        onClick={toggle}
        className={`toggle-btn ${isOn ? 'on' : 'off'}`}
      >
        {isOn ? 'オン' : 'オフ'}
      </button>
    </div>
  )
}

export default Toggle

TypeScript版:

TSX
import { type JSX, useState } from 'react'

function Toggle(): JSX.Element {
  const [isOn, setIsOn] = useState<boolean>(false)
  
  const toggle = (): void => {
    setIsOn(!isOn)
  }
  
  return (
    <div>
      <h2>ライト: {isOn ? 'ON' : 'OFF'}</h2>
      <button 
        onClick={toggle}
        className={`toggle-btn ${isOn ? 'on' : 'off'}`}
      >
        {isOn ? 'オン' : 'オフ'}
      </button>
    </div>
  )
}

export default Toggle

複雑な状態の管理

💡 このセクションで学ぶこと:

オブジェクトや配列などの複雑なデータ構造を状態として扱う方法を習得します。

「フォームデータ」や「Todoリスト」など、実務で必須となるスキルが身につきます。

オブジェクトの状態

複数の関連するデータをまとめて管理したい場合、オブジェクトを使います。

例えば、ユーザープロフィールのように、名前、メールアドレス、年齢などの複数の情報を一つの状態で管理します。

以下のサンプルコードでは、次の3つの重要なテクニックに注目してください:

  1. スプレッド構文(...user既存の複数のプロパティを保持しながら更新する
  2. 計算プロパティ名([name]: value で動的に入力に対応するプロパティだけを更新する
  3. 1つのhandleChange関数で全ての入力フィールドに対応する(name属性が鍵)

JavaScript版:

JSX
import { useState } from 'react'

function UserProfile() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: ''
  })
  
  const handleChange = (e) => {
    const { name, value } = e.target
    setUser({
      ...user,  // 既存のプロパティをコピー
      [name]: value  // 対応するプロパティを更新
    })
  }
  
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('ユーザー情報:', user)
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={user.name}
        onChange={handleChange}
        placeholder="名前"
      />
      <input
        type="email"
        name="email"
        value={user.email}
        onChange={handleChange}
        placeholder="メール"
      />
      <input
        type="number"
        name="age"
        value={user.age}
        onChange={handleChange}
        placeholder="年齢"
      />
      <button type="submit">送信</button>
    </form>
  )
}
export default UserProfile

12行目のsetUser()の引数の部分について、もう少し考えを整理すると

  • {}で新しい空のオブジェクト生成
  • ...userで以前の情報を作成したオブジェクト内に展開
  • [name]: valueをオブジェクトに渡す(nameと同名のプロパティがあれば上書き)

のように展開され、結果的に該当するプロパティだけ値が変更されたオブジェクトがsetUser()に渡されます。

TypeScript版:

TSX
import { type JSX, useState } from 'react'

interface User {
  name: string
  email: string
  age: string
}

function UserProfile(): JSX.Element {
  const [user, setUser] = useState<User>({
    name: '',
    email: '',
    age: '',
  })

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    const { name, value } = e.target
    setUser({
      ...user,
      [name]: value,
    })
  }

  const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>): void => {
    e.preventDefault()
    console.log('ユーザー情報:', user)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={user.name}
        onChange={handleChange}
        placeholder="名前"
      />
      <input
        type="email"
        name="email"
        value={user.email}
        onChange={handleChange}
        placeholder="メール"
      />
      <input
        type="number"
        name="age"
        value={user.age}
        onChange={handleChange}
        placeholder="年齢"
      />
      <button type="submit">送信</button>
    </form>
  )
}
export default UserProfile

配列の状態

Todoリスト商品リストなど、複数のアイテムを管理する場合は配列を使います。

配列の状態を扱う際の最も重要なルールは、元の配列を変更せず、常に新しい配列を作成することです。

以下のサンプルコードでは、配列の3つの基本操作に注目してください:

  1. 追加[...todos, newTodo] で新しい配列を作成して末尾に追加
  2. 更新todos.map() で条件に合うアイテムだけを更新
  3. 削除todos.filter() で条件に合うアイテム以外を残す
配列操作の基本パターン
操作❌ 悪い例(破壊的)✅ 良い例(非破壊的)
追加array.push(item)[...array, item]
削除array.splice(index, 1)array.filter((_, i) => i !== index)
更新array[index] = newValuearray.map((item, i) => i === index ? newValue : item)

破壊的メソッド元の配列を変更するため、Reactでは使ってはいけません。

ステートで管理している変数を変更できるのはsetXXXメソッドだけです。

JavaScript版:

JSX
import { useState } from 'react'

function TodoList() {
  const [todos, setTodos] = useState([])
  const [inputValue, setInputValue] = useState('')
  
  // 新しいTodoを追加
  const addTodo = () => {
    if (inputValue.trim()) {
      const newTodo = {
        id: Date.now(),
        text: inputValue,
        completed: false
      }
      setTodos([...todos, newTodo])
      setInputValue('')
    }
  }
  
  // Todoを完了/未完了に切り替え
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ))
  }
  
  // Todoを削除
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }
  
  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
        placeholder="新しいTodoを入力"
      />
      <button onClick={addTodo}>追加</button>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span 
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none'
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default TodoList

TypeScript版:

TSX
import { type JSX, useState } from 'react'

interface Todo {
  id: number
  text: string
  completed: boolean
}

function TodoList(): JSX.Element {
  const [todos, setTodos] = useState<Todo[]>([])
  const [inputValue, setInputValue] = useState<string>('')
  
  const addTodo = (): void => {
    if (inputValue.trim()) {
      const newTodo: Todo = {
        id: Date.now(),
        text: inputValue,
        completed: false
      }
      setTodos([...todos, newTodo])
      setInputValue('')
    }
  }
  
  const toggleTodo = (id: number): void => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ))
  }
  
  const deleteTodo = (id: number): void => {
    setTodos(todos.filter(todo => todo.id !== id))
  }
  
  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
        placeholder="新しいTodoを入力"
      />
      <button onClick={addTodo}>追加</button>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span 
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none'
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default TodoList

複数の状態を管理する

💡 このセクションで学ぶこと:

1つのコンポーネントで複数の状態を扱う方法を習得します。

関連する状態をどう整理するか、実践的な判断基準が身につきます。

複数のuseStateを使う

1つのコンポーネントで複数の状態を管理する必要がある場合、複数のuseStateを使います。

複数状態の使い分け基準
  • 関連性が低い別々のuseStateにする
  • 常に一緒に更新される 1つのuseStateにまとめる
  • 独立して更新される別々のuseStateにする

JavaScript版:

JSX
import { useState } from 'react'

function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState('')
  
  const handleSubmit = async (e) => {
    e.preventDefault()
    setError('')
    setIsLoading(true)
    
    try {
      // ログイン処理(シミュレーション)
      await new Promise(resolve => setTimeout(resolve, 1000))
      console.log('ログイン成功:', email)
    } catch (err) {
      setError('ログインに失敗しました')
    } finally {
      setIsLoading(false)
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="メール"
        disabled={isLoading}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="パスワード"
        disabled={isLoading}
      />
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'ログイン中...' : 'ログイン'}
      </button>
    </form>
  )
}

export default LoginForm

TypeScript版:

TSX
import { type JSX, useState } from 'react'

function LoginForm(): JSX.Element {
  const [email, setEmail] = useState<string>('')
  const [password, setPassword] = useState<string>('')
  const [isLoading, setIsLoading] = useState<boolean>(false)
  const [error, setError] = useState<string>('')
  
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
    e.preventDefault()
    setError('')
    setIsLoading(true)
    
    try {
      await new Promise(resolve => setTimeout(resolve, 1000))
      console.log('ログイン成功:', email)
    } catch (err) {
        if (err instanceof Error) {
          setError('ログインに失敗しました')
        } else {
          setError('予期せぬエラーが発生しました')
        }
    } finally {
      setIsLoading(false)
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="メール"
        disabled={isLoading}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="パスワード"
        disabled={isLoading}
      />
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'ログイン中...' : 'ログイン'}
      </button>
    </form>
  )
}

export default LoginForm

更新関数の2つの形式

💡 このセクションで学ぶこと:

状態更新の2つの方法(直接値を渡す方法と関数を渡す方法)を理解し、どちらを使うべきか判断できるようになります。

特に、関数形式がなぜ重要なのかを深く理解します。

前の状態に基づいて更新する場合

状態更新には2つの形式があります。

前の状態に依存する更新では、関数形式を使うことが強く推奨されます

形式1: 直接値を渡す
JSX
setCount(5)           // カウントを5にセット
setName('太郎')       // 名前を'太郎'にセット

使うべき場面: 前の値に関係なく、新しい値をセットする場合

形式2: 関数を渡す(推奨)
JSX
setCount(prevCount => prevCount + 1)        // 前の値に+1
setItems(prevItems => [...prevItems, newItem])  // 配列に追加

使うべき場面: 前の状態に基づいて更新する場合(こちらが推奨)

なぜ関数形式が重要なのか

Reactの状態更新には重要な特徴があります:

  1. 状態更新は非同期:setStateを呼んでもすぐには反映されない
  2. バッチ処理:複数の更新がまとめて処理される
  3. クロージャの問題:古い値を参照してしまう可能性がある
❌ 悪い例(直接値を使う)
JSX
const increment = () => {
  setCount(count + 1)
  setCount(count + 1)
  setCount(count + 1)
  // 期待: count = 3
  // 実際: count = 1(3つのsetCountが全て同じcountを参照)
}

何が起こっているか:

  • 3つのsetCount(count + 1)が全て「現在のcount(例えば0)」を参照
  • setCount(0 + 1)が3回呼ばれるだけ
  • 結果は1になる
✅ 良い例(関数形式を使う)
JSX
const increment = () => {
  setCount(prev => prev + 1)
  setCount(prev => prev + 1)
  setCount(prev => prev + 1)
  // 結果: count = 3(確実に)
}

何が起こっているか:

  • 1回目:prevは0、結果は1
  • 2回目:prevは1(更新された値)、結果は2
  • 3回目:prevは2(更新された値)、結果は3
  • 正しく3になる

実践的な例

*UI部分は割愛しています。

JavaScript版:

JSX
import { useState } from 'react'

function ShoppingCart() {
  const [cart, setCart] = useState([])

  // ✅ 良い例:関数形式で前の状態を使う
  const addItem = (item) => {
    setCart(prevCart => [...prevCart, item])
  }

  const updateQuantity = (id, quantity) => {
    setCart(prevCart =>
      prevCart.map(item =>
        item.id === id ? { ...item, quantity } : item
      )
    )
  }

  const removeItem = (id) => {
    setCart(prevCart => prevCart.filter(item => item.id !== id))
  }

  return (
    <div>
      <h2>カート内のアイテム: {cart.length}</h2>
      {/* UI */}
    </div>
  )
}

export default ShoppingCart

TypeScript版:

TSX
import { type JSX , useState } from 'react'

interface CartItem {
  id: number
  name: string
  quantity: number
}

function ShoppingCart(): JSX.Element {
  const [cart, setCart] = useState<CartItem[]>([])

  const addItem = (item: CartItem): void => {
    setCart(prevCart => [...prevCart, item])
  }

  const updateQuantity = (id: number, quantity: number): void => {
    setCart(prevCart =>
      prevCart.map(item =>
        item.id === id ? { ...item, quantity } : item
      )
    )
  }

  const removeItem = (id: number): void => {
    setCart(prevCart => prevCart.filter(item => item.id !== id))
  }

  return (
    <div>
      <h2>カート内のアイテム: {cart.length}</h2>
      {/* UI */}
    </div>
  )
}

export default ShoppingCart

💡 ベストプラクティス:

前の状態を使う更新は、常に関数形式を使いましょう。

安全で予測可能なコードになります。

stateを安全に使う

状態の更新の原則

💡 このセクションで学ぶこと:

状態を正しく更新するための3つの重要な原則を理解します。

イミュータブル(不変性)の概念を学び、なぜそれが重要なのか、どう実践するのかが明確になります。

原則1: 状態を直接変更しない(イミュータブル)

Reactでは、状態を直接変更してはいけません

これは最も重要なルールの1つです。

❌ 悪い例:

JSX
const [user, setUser] = useState({ name: '太郎', age: 25 })

// 直接変更(これは動作しません)
user.age = 26       // ❌ NG
user.name = '花子'    // ❌ NG

✅ 良い例:

JSX
const [user, setUser] = useState({ name: '太郎', age: 25 })

// 新しいオブジェクトを作成
setUser({ ...user, age: 26 })              // ✅ OK
setUser(prev => ({ ...prev, name: '花子' }))  // ✅ さらに安全

なぜ直接変更してはいけないのか?

Reactの”変更”検知の仕組み

  • Reactは状態オブジェクトの参照(メモリアドレス)で変化を検出します
  • 直接変更しても参照は変わらないため、Reactは変化に気づきません
  • 結果、画面が更新されません

予期しないバグの原因

  • 状態が変わったのに画面が更新されない
  • 他の部分に影響が及ぶ
  • デバッグを見つけるのが困難になる

原則2: 配列の更新でも新しい配列を作成

配列も同様に、破壊的メソッドを避け、破壊的メソッドを使います。

❌ 悪い例(破壊的メソッド):

JSX
const [items, setItems] = useState([1, 2, 3])

// これらは全て元の配列を変更してしまう
items.push(4)          // ❌ NG
items.pop()            // ❌ NG
items.splice(0, 1)     // ❌ NG
items[0] = 10          // ❌ NG
items.sort()           // ❌ NG
items.reverse()        // ❌ NG

✅ 良い例(非破壊的メソッド):

JSX
const [items, setItems] = useState([1, 2, 3])

// 新しい配列を作成するメソッド
setItems([...items, 4])                    // ✅ 末尾に追加
setItems([0, ...items])                    // ✅ 先頭に追加
setItems(items.concat(4))                  // ✅ concat()
setItems(items.slice(0, -1))               // ✅ 末尾を削除
setItems(items.filter((_, i) => i !== 0))  // ✅ 最初を削除
setItems(items.map((item, i) => i === 0 ? 10 : item))  // ✅ 更新
setItems([...items].sort())                // ✅ ソート
setItems([...items].reverse())             // ✅ 反転
配列操作の比較表
操作❌ 破壊的(NG)✅ 非破壊的(OK)
末尾に追加array.push(item)[...array, item]
先頭に追加array.unshift(item)[item, ...array]
末尾を削除array.pop()array.slice(0, -1)
先頭を削除array.shift()array.slice(1)
特定要素を削除array.splice(i, 1)array.filter((_, index) => index !== i)
特定要素を更新array[i] = valuearray.map((item, index) => index === i ? value : item)
ソートarray.sort()[...array].sort()
反転array.reverse()[...array].reverse()

原則3: 不変性(Immutability)の重要性

不変性とは、一度作成したデータを変更せず、変更が必要な場合は新しいデータを作成するという考え方です。

なぜ不変性が重要なのか
  1. 予測可能性: 状態が予期せず変わることがない
  2. デバッグの容易さ: 状態の変更履歴を追跡しやすい
  3. パフォーマンス最適化: 参照の比較だけで変更検知できる
  4. タイムトラベルデバッグ: 過去の状態に戻ることができる
深いネストの更新:

ネストが深いオブジェクトを更新する際も、不変性を保つ必要があります。

JavaScript版:

JSX
import { useState } from 'react'

function Profile() {
  const [profile, setProfile] = useState({
    user: {
      personal: {
        name: '太郎',
        age: 25
      },
      contact: {
        email: 'taro@example.com'
      }
    }
  })

  // 深くネストされたプロパティを更新
  const updateName = (newName) => {
    setProfile({
      ...profile,                    // 最上位をコピー
      user: {
        ...profile.user,             // userをコピー
        personal: {
          ...profile.user.personal,  // personalをコピー
          name: newName              // nameだけ更新
        }
      }
    })
  }

  return (
    <div>
      <h2>{profile.user.personal.name}</h2>
      <button onClick={() => updateName('花子')}>
        名前を変更
      </button>
    </div>
  )
}

💡 ヒント:

ネストが深すぎる場合は、状態の構造を見直すか、後々学習するuseReducerや状態管理ライブラリの使用を検討しましょう。

useStateの落とし穴と注意点

💡 このセクションで学ぶこと:

初心者が必ず遭遇する3つの落とし穴とその対策を習得します。バッチ処理、非同期更新、初期化関数の使い方を理解し、予期しないバグを回避できるようになります。

落とし穴1: 状態更新はバッチ処理される

Reactは、パフォーマンスのために複数の状態更新をバッチ処理(まとめて処理)します。

🤷‍♂️ 何が起こるのか?

同じイベントハンドラ内で複数回setStateを呼んでも、再レンダリングは1回だけ行われます。

JavaScript版:

JSX
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    // 期待: count = 3
    // 実際: count = 1
    // 理由: 3つのsetCountが全て同じcount(0)を参照している
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+3?</button>
    </div>
  )
}

解決策:関数形式を使う

JSX
function CounterFixed() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(prev => prev + 1)  // 1回目: 0 + 1 = 1
    setCount(prev => prev + 1)  // 2回目: 1 + 1 = 2
    setCount(prev => prev + 1)  // 3回目: 2 + 1 = 3
    // 結果: count = 3(正しい)
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+3</button>
    </div>
  )
}

💡 重要:

前の状態に基づく更新は、必ず関数形式を使いましょう。

落とし穴2: 状態更新は非同期

状態の更新はすぐには反映されません

setStateを呼んだ直後は、まだ古い値のままです。

問題が起きる例

JavaScript版:

JSX
import { useState } from 'react'

function Example() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
    console.log(count)  // 0 が表示される(更新前の値)
    
    // この時点ではまだcountは更新されていない
    if (count === 5) {  // これは期待通りに動かない
      alert('5になりました')
    }
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>クリック</button>
    </div>
  )
}

解決策1:関数形式で新しい値を使う

JSX
function ExampleFixed() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(prev => {
      const newCount = prev + 1
      console.log(newCount)  // 1 が表示される(新しい値)
      
      // 新しい値を使った処理
      if (newCount === 5) {
        alert('5になりました')
      }
      
      return newCount
    })
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>クリック</button>
    </div>
  )
}

解決策2:useEffectで監視する

useEffectはReact Hooksの一種です。別の記事で紹介する予定なので後で深く学びます。

現時点では、”問題を解決手段が他にもある事”を覚えておくぐらいで良いです。

JSX
import { useState, useEffect } from 'react'

function ExampleWithEffect() {
  const [count, setCount] = useState(0)

  // countが変わったら実行される
  useEffect(() => {
    if (count === 5) {
      alert('5になりました')
    }
  }, [count])  // countを監視

  const handleClick = () => {
    setCount(count + 1)
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>クリック</button>
    </div>
  )
}

落とし穴3: 初期化関数内での重い処理

useStateの初期値に関数を直接呼ぶと、毎回のレンダリングで実行されてしまいます

❌ 悪い例:毎回実行される

JSX
function HeavyComponent() {
  // この関数は毎回のレンダリングで実行される!
  const [data, setData] = useState(expensiveComputation())
  
  return <div>{data}</div>
}

function expensiveComputation() {
  console.log('重い処理を実行中...')
  // 時間のかかる処理
  let result = 0
  for (let i = 0; i < 1000000; i++) {
    result += i
  }
  return result
}

🤷‍♂️ 何が問題か:

  • コンポーネントが再レンダリングされるたび(例えば親の状態変更など)にexpensiveComputation()が実行される
  • パフォーマンスが著しく低下する

つまり、ステートの初期値が1回目以降は無視されるのに、毎回計算してしまうという無駄が発生します。

✅ 良い例:

JSX
function OptimizedComponent() {
  // 関数を渡すことで、最初の1回だけ実行される
  const [data, setData] = useState(() => expensiveComputation())
  
  return <div>{data}</div>
}

function expensiveComputation() {
  console.log('重い処理を実行中...(1回だけ)')
  let result = 0
  for (let i = 0; i < 1000000; i++) {
    result += i
  }
  return result
}

どう動作するか:

  • () => expensiveComputation()という関数を渡す
  • Reactはこの関数をコンポーネントの初回レンダリング時のみ実行する
  • 2回目以降のレンダリングでは、この関数は無視される

💡 パフォーマンステクニック:

初期値の計算が重い場合(ローカルストレージから読み込む、大量のデータを処理するなど)は、必ず初期化関数() => 値)を使いましょう。

なぜ後者は1回しか実行されないのか?:

❶ 前者はuseState()の引数に関数実行結果の値を渡している

❷ 後者はuseState()の引数に関数そのモノを”値”として渡している。(「手順書を渡しただけ」みたいなイメージ)

Reactは「関数が渡された」と認識すると、特別な処理をします。(初回だけ計算)

実践:フィルタリング可能な商品リスト

💡 このセクションで学ぶこと:

これまで学んだ全ての知識を統合した実践的なアプリケーションを作ります。

フィルタリング、ソート、統計表示などの機能を実装し、実務レベルのコードが書けるようになります。

完全な例として、フィルタリング機能付きの商品リストを作ってみましょう。

💡 この例で学べること:

  • 複数の状態を組み合わせたフィルタリング
  • 配列のメソッドチェーン(filter → sort)
  • 計算された値(filteredProducts)の使い方
  • 条件付きレンダリング(該当商品がない場合の表示)

JavaScript版:

JSX
import { useState } from 'react'

function ProductFilter() {
  const [products] = useState([
    { id: 1, name: 'ノートPC', price: 120000, category: '電子機器' },
    { id: 2, name: 'マウス', price: 3000, category: '電子機器' },
    { id: 3, name: 'ノート', price: 500, category: '文房具' },
    { id: 4, name: 'ペン', price: 200, category: '文房具' },
    { id: 5, name: 'キーボード', price: 8000, category: '電子機器' }
  ])

  const [selectedCategory, setSelectedCategory] = useState('全て')
  const [priceRange, setPriceRange] = useState([0, 150000])
  const [sortBy, setSortBy] = useState('name')

  // フィルタリングされた商品を取得
  const filteredProducts = products
    .filter(product =>
      (selectedCategory === '全て' || product.category === selectedCategory) &&
      product.price >= priceRange[0] &&
      product.price <= priceRange[1]
    )
    .sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name)
      } else if (sortBy === 'price-asc') {
        return a.price - b.price
      } else {
        return b.price - a.price
      }
    })

  return (
    <div className="product-filter">
      <h1>商品一覧</h1>

      {/* フィルター */}
      <div className="filters">
        <div>
          <label>カテゴリ:</label>
          <select 
            value={selectedCategory}
            onChange={(e) => setSelectedCategory(e.target.value)}
          >
            <option>全て</option>
            <option>電子機器</option>
            <option>文房具</option>
          </select>
        </div>

        <div>
          <label>価格範囲: ¥{priceRange[0].toLocaleString()} - ¥{priceRange[1].toLocaleString()}</label>
          <input
            type="range"
            min="0"
            max="150000"
            value={priceRange[1]}
            onChange={(e) => setPriceRange([0, parseInt(e.target.value)])}
          />
        </div>

        <div>
          <label>ソート:</label>
          <select 
            value={sortBy}
            onChange={(e) => setSortBy(e.target.value)}
          >
            <option value="name">名前順</option>
            <option value="price-asc">価格が安い順</option>
            <option value="price-desc">価格が高い順</option>
          </select>
        </div>
      </div>

      {/* 商品リスト */}
      <div className="products">
        {filteredProducts.length === 0 ? (
          <p>該当する商品がありません</p>
        ) : (
          <ul>
            {filteredProducts.map(product => (
              <li key={product.id} className="product-item">
                <h3>{product.name}</h3>
                <p>¥{product.price.toLocaleString()}</p>
                <p className="category">{product.category}</p>
              </li>
            ))}
          </ul>
        )}
      </div>

      <p>表示件数: {filteredProducts.length}</p>
    </div>
  )
}

export default ProductFilter

TypeScript版:

TSX
import { type JSX, useState } from 'react'

interface Product {
  id: number
  name: string
  price: number
  category: string
}

type SortBy = 'name' | 'price-asc' | 'price-desc'

function ProductFilter(): JSX.Element {
  const [products] = useState<Product[]>([
    { id: 1, name: 'ノートPC', price: 120000, category: '電子機器' },
    { id: 2, name: 'マウス', price: 3000, category: '電子機器' },
    { id: 3, name: 'ノート', price: 500, category: '文房具' },
    { id: 4, name: 'ペン', price: 200, category: '文房具' },
    { id: 5, name: 'キーボード', price: 8000, category: '電子機器' }
  ])

  const [selectedCategory, setSelectedCategory] = useState<string>('全て')
  const [priceRange, setPriceRange] = useState<[number, number]>([0, 150000])
  const [sortBy, setSortBy] = useState<SortBy>('name')

  const filteredProducts = products
    .filter((product: Product) =>
      (selectedCategory === '全て' || product.category === selectedCategory) &&
      product.price >= priceRange[0] &&
      product.price <= priceRange[1]
    )
    .sort((a: Product, b: Product) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name)
      } else if (sortBy === 'price-asc') {
        return a.price - b.price
      } else {
        return b.price - a.price
      }
    })

  return (
    <div className="product-filter">
      <h1>商品一覧</h1>

      <div className="filters">
        <div>
          <label>カテゴリ:</label>
          <select 
            value={selectedCategory}
            onChange={(e) => setSelectedCategory(e.target.value)}
          >
            <option>全て</option>
            <option>電子機器</option>
            <option>文房具</option>
          </select>
        </div>

        <div>
          <label>価格範囲: ¥{priceRange[0].toLocaleString()} - ¥{priceRange[1].toLocaleString()}</label>
          <input
            type="range"
            min="0"
            max="150000"
            value={priceRange[1]}
            onChange={(e) => setPriceRange([0, parseInt(e.target.value)])}
          />
        </div>

        <div>
          <label>ソート:</label>
          <select 
            value={sortBy}
            onChange={(e) => setSortBy(e.target.value as SortBy)}
          >
            <option value="name">名前順</option>
            <option value="price-asc">価格が安い順</option>
            <option value="price-desc">価格が高い順</option>
          </select>
        </div>
      </div>

      <div className="products">
        {filteredProducts.length === 0 ? (
          <p>該当する商品がありません</p>
        ) : (
          <ul>
            {filteredProducts.map((product: Product) => (
              <li key={product.id} className="product-item">
                <h3>{product.name}</h3>
                <p>¥{product.price.toLocaleString()}</p>
                <p className="category">{product.category}</p>
              </li>
            ))}
          </ul>
        )}
      </div>

      <p>表示件数: {filteredProducts.length}</p>
    </div>
  )
}

export default ProductFilter

よくあるuseStateの質問

💡 このセクションで学ぶこと:

開発現場でよく直面する疑問とその解決策を習得します。

useStateをどこで呼ぶべきか、propsとの関係、状態の整理方法など、実践的な判断基準が身につきます。

Q1: useStateはどこで呼び出す?

useStateはReactのフックなので、呼び出せる場所に制限があります。

✅ 正しい場所:

JSX
function Component() {
  // ✅ コンポーネント本体の最上部
  const [count, setCount] = useState(0)
  const [name, setName] = useState('')
  
  return <div>{count} - {name}</div>
}

❌ 間違った場所:

JSX
// ❌ 条件内(NG)
function Component() {
  if (condition) {
    const [count, setCount] = useState(0)  // エラー
  }
}

// ❌ ループ内(NG)
function Component() {
  for (let i = 0; i < 5; i++) {
    const [count, setCount] = useState(0)  // エラー
  }
}

// ❌ イベントハンドラ内(NG)
function Component() {
  const handleClick = () => {
    const [count, setCount] = useState(0)  // エラー
  }
}

// ❌ 条件付きで呼び出し(NG)
function Component() {
  const [count, setCount] = condition ? useState(0) : useState(1)  // エラー
}

なぜこのルールがあるのか?

ReactはuseStateが呼ばれる順序に依存して、どの状態がどの変数に対応するかを管理しています。

JSX
function Component() {
  // React内部:1番目のuseState → countに対応
  const [count, setCount] = useState(0)
  
  // React内部:2番目のuseState → nameに対応
  const [name, setName] = useState('')
  
  // この順序が変わると、Reactが混乱してバグが発生
}

条件付きで呼ぶと、レンダリングごとに順序が変わってしまい、Reactが正しく管理できなくなります。

💡 重要:

useStateは必ずコンポーネントの最上位で、同じ順序で呼び出しましょう。

Q2: useStateの初期値にはいつpropsを使う?

propsをuseStateの初期値に使うかどうかは、propsの変更を追跡したいかによります。

重要:propsは関数でなくても1回だけ使われる

まず、混乱しやすいポイントを明確にしましょう:

JSX
// これは1回だけ評価される(propsは関数ではないので)
const [count, setCount] = useState(initialCount)

// これも1回だけ評価される(0は関数ではないので)
const [count, setCount] = useState(0)

// これは毎回実行される(関数を実行した結果を渡しているから)
const [count, setCount] = useState(expensiveComputation())

// これは1回だけ実行される(関数自体を渡しているから)
const [count, setCount] = useState(() => expensiveComputation())

ポイント:

  • 値を渡す場合(数値、文字列、propsなど)→ その値は1回だけ使われる
  • 関数を呼び出して結果を渡す場合func())→ 毎回実行される
  • 関数自体を渡す場合() => func())→ 1回だけ実行される

なぜpropsは1回だけ使われるのか?

Reactの内部処理(イメージ):

JSX
function useState(initialValue) {
  if (この状態がまだ存在しない) {
    // 初回のみ:initialValueを使って状態を作成
    状態を作成(initialValue)
  } else {
    // 2回目以降:initialValueは完全に無視される
    既存の状態を返す
  }
}
ケース1: 初期値として使うだけ(OK)
JSX
function Counter({ initialCount }) {
  // 最初の1回だけpropsを使う
  const [count, setCount] = useState(initialCount)
  
  // その後propsが変わってもcountは変わらない
  // これが意図した動作ならOK
  return <div>{count}</div>
}

使う場面: 「初期値だけpropsから受け取って、その後は独自に管理したい」場合

ケース2: propsの変更を反映したい(useEffectが必要)
JSX
import { useState, useEffect } from 'react'

function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount)
  
  // propsが変わったら状態も更新
  useEffect(() => {
    setCount(initialCount)
  }, [initialCount])
  
  return <div>{count}</div>
}

使う場面: 「propsが変わったら状態も更新したい」場合

ケース3: そもそも状態が不要(最善)
JSX
// propsをそのまま使う(状態管理不要)
function Counter({ count }) {
  return <div>{count}</div>
}

使う場面: 「propsをそのまま表示するだけ」の場合

💡 判断基準: propsをそのまま表示するだけなら、状態にする必要はありません。状態は「変更が必要な場合」のみ使いましょう。

Q3: 複数の関連する状態をまとめるべき?

関連する状態をまとめるか分けるかは、更新パターンで判断します。

まとめるべき場合

条件: 常に一緒に更新される

JSX
// ✅ 良い例:座標は一緒に更新される
const [position, setPosition] = useState({ x: 0, y: 0 })

const handleMove = (newX, newY) => {
  setPosition({ x: newX, y: newY })  // 一度に更新
}
分けるべき場合

条件: 独立して更新される

JSX
// ✅ 良い例:独立して更新される
const [count, setCount] = useState(0)
const [name, setName] = useState('')

setCount(count + 1)  // countだけ更新
setName('太郎')      // nameだけ更新
判断表
状態の関係判断理由
常に一緒に更新✅ まとめる更新ロジックがシンプルになる
一緒に更新されることが多い✅ まとめる関連性が明確になる
独立して更新❌ 分ける不要な再レンダリングを防ぐ
片方だけ頻繁に更新❌ 分けるパフォーマンスが向上

💡 ベストプラクティス:

迷ったらまず分けて、問題があればまとめる方が安全です。

useStateのパフォーマンス最適化

💡 このセクションで学ぶこと:

useStateを使う上で知っておくべき基本的なパフォーマンス最適化テクニックを習得します。

より高度な最適化は、別記事の#17で学びますが、ここでは今すぐ使える基本を押さえます。

不要な再レンダリングを避ける

1. オブジェクトを外で定義する

コンポーネント内でオブジェクトや配列を定義すると、毎回新しいインスタンスが作られます。

❌ 非効率:毎回新しいオブジェクトを作成

JSX
function App() {
  const [data, setData] = useState({ items: [1, 2, 3] })
  
  // 毎回新しいオブジェクトが作成される
  const defaultData = { items: [1, 2, 3] }
  
  return <Child data={defaultData} />
}

✅ 効率的:オブジェクトを外で定義

JSX
// コンポーネントの外で定義
const defaultData = { items: [1, 2, 3] }

function App() {
  const [data, setData] = useState(defaultData)
  
  return <Child data={defaultData} />
}
2. 状態を適切に分割する

関連性の低い状態を1つにまとめると、不要な再レンダリングが発生します。

❌ 非効率:全てを1つのオブジェクトに

JSX
function App() {
  const [state, setState] = useState({
    count: 0,
    name: '',
    email: '',
    isLoading: false
  })
  
  // nameを変更しただけで、全ての状態が更新される
  const updateName = (name) => {
    setState({ ...state, name })
  }
}

✅ 効率的:状態を分割

JSX
function App() {
  // 関連する状態ごとに分ける
  const [count, setCount] = useState(0)
  const [userInfo, setUserInfo] = useState({ name: '', email: '' })
  const [isLoading, setIsLoading] = useState(false)
  
  // nameの変更はuserInfoだけに影響
  const updateName = (name) => {
    setUserInfo(prev => ({ ...prev, name }))
  }
}
3. 初期化関数を使う

重い計算は初期化関数で1回だけ実行するようにします。

❌ 非効率:毎回実行される

JSX
function App() {
  // 毎回のレンダリングで実行される
  const [data, setData] = useState(heavyComputation())
}

✅ 効率的:初回のみ実行

JSX
function App() {
  // 初回のみ実行される
  const [data, setData] = useState(() => heavyComputation())
}

より高度な最適化について

React.memouseMemouseCallbackなどのより高度な最適化テクニックは、#17 パフォーマンス最適化で詳しく解説します。

基本的な最適化ポイント:

  • 状態を適切に分割する
  • 初期化関数を活用する
  • 不要なオブジェクト生成を避ける

これらを意識するだけでも、多くのパフォーマンス問題を防げます。

まとめ

この記事では、useStateを使った状態管理の基本を詳しく学びました。

👉 重要なポイント:

  • useStateは関数コンポーネントに状態管理機能を追加
  • 状態を更新するとコンポーネントが再レンダリングされる
  • 状態は読み取り専用のpropsと異なり、変更可能
  • 状態の更新は不変性を守る(新しいオブジェクト/配列を作成)
  • 関数形式の更新で前の状態に基づいた更新が可能

👍 ベストプラクティス:

  • useStateは関数の最上部で呼び出す
  • 関連する状態は一つのuseStateにまとめる
  • 更新が前の状態に依存する場合は関数形式を使う
  • イミュータブルな方法で状態を更新する
  • 初期化が重い場合は初期化関数を使う

記事内の各セクションを説明するのに「イミュータブル」や「配列の扱い(非破壊的操作)」などについて、重複して同じ対処法が説明に出てきました。

違う見出しのセクションを説明するにも、それぐらい共通して重要なテクニックだったためです。

ステートを安全に使うには、何度も考えを整理しつつ、実際に書いて慣れる事が上達の近道になると思います。

次のステップ: 次回は、イベント処理の詳細について学びます。useStateと組み合わせることで、ユーザーの操作に応答するインタラクティブなアプリケーションが作れるようになります。

useStateは、Reactの最も基本的で最も重要なHooksです。様々なパターンを試しながら、状態管理に慣れていくことが上達の鍵です!