メインコンテンツまでスキップ

State(useState)

この章では、コンポーネントの状態を管理するStateについて学びます。

この章で学ぶこと

  • Stateとは何か、なぜ必要か
  • useStateフックの使い方
  • 配列・オブジェクトのState更新
  • StateとPropsの違い
備考

この章では03章で作成したプロジェクトを使用します。npm run devで開発サーバーを起動し、コードを書きながら学習を進めましょう。

備考

この章のコード例は、そのままApp.tsxにコピーして動作確認できます。

Stateとは

Stateは、コンポーネント内部で管理される変更可能なデータです。Stateが更新されると、コンポーネントが再レンダリングされます。

備考

なぜStateが必要なのでしょうか?通常の変数では、値を変更してもReactは変更を検知できず、画面が更新されません。Stateを使うことで、値の変更をReactに伝え、UIを自動的に更新できます。

useStateフック

ReactではuseStateフックを使ってStateを管理します。

useStateを呼ぶと、2つのものがセットで返ってきます:

  1. 現在の値(読み取り用)- 今の状態を取得
  2. 更新関数(書き込み用)- 状態を変更し、画面を再描画
const [count, setCount] = useState(0)
// ↑読み取り ↑書き込み ↑初期値
import { useState } from 'react'
import './App.css'

function Counter() {
// useState は [現在の値, 更新関数] の配列を返す
// 配列の分割代入で count と setCount に代入
const [count, setCount] = useState(0)

// ボタンクリック時に呼ばれる関数
const increment = () => {
// prev には現在の count の値が渡される
// prev + 1 の結果が新しい count になる
setCount(prev => prev + 1)
}

return (
<div>
{/* count の値が変わると、この部分が自動で更新される */}
<p>カウント: {count}</p>
<button onClick={increment}>+1</button>
</div>
)
}

export default function App() {
return <Counter />
}

setCountには値を直接渡すこともできますが、関数を渡すこともできます。関数を渡すと、Reactがその関数に現在の値を引数として渡して実行します。

setCount(prev => prev + 1)
↑ ↑
│ └─ 戻り値 = 新しいstate値
└─ Reactが現在の値(count)を渡してくれる

useStateの構文

useState配列を返します。この配列を分割代入で受け取ります。

const [state, setState] = useState(initialValue)
要素説明先ほどの例
state現在の状態値count
setState状態を更新する関数setCount
initialValue初期値0

命名の慣習

状態名にsetを付けたものが更新関数名になります:

const [count, setCount] = useState(0) // カウンター
const [name, setName] = useState('') // 名前
const [isOpen, setIsOpen] = useState(false) // 開閉状態

※配列の分割代入については02章で解説しています。

型の指定

TypeScriptでは、初期値から型が推論されます。

// 型推論される(初期値から自動で型が決まる)
const [count, setCount] = useState(0) // number型と推論
const [name, setName] = useState('') // string型と推論
const [isActive, setIsActive] = useState(false) // boolean型と推論

// 明示的に型を指定(初期値だけでは意図した型にならない場合)
// <User | null> は「User型 または null」という意味
const [user, setUser] = useState<User | null>(null)
// <string[]> は「文字列の配列」という意味
const [items, setItems] = useState<string[]>([])

useState<User | null>(null)<User | null>はジェネリクスと呼ばれる構文です。初期値がnullだけだとnull型と推論されてしまい、あとからUserオブジェクトを入れられないため、「User型またはnull」と明示しています。

初期値の計算が重い場合(遅延初期化)

useStateに値を直接渡すと、その値はレンダーのたびに評価されます。初期値が使われるのは初回レンダーだけですが、localStorage からの読み込みや重い計算を直接渡すと、以降のレンダーでも毎回実行されて無駄になります。

// 毎レンダーで JSON.parse が実行されてしまう(無駄)
const [items, setItems] = useState(JSON.parse(localStorage.getItem('items') ?? '[]'))

このような場合は、useState に関数を渡すことで初回レンダー時のみその関数が実行されるようにできます。これを遅延初期化と呼びます。

// 初回レンダー時のみ JSON.parse が実行される
const [items, setItems] = useState(() => {
return JSON.parse(localStorage.getItem('items') ?? '[]')
})
備考

遅延初期化が有効なのは、初期値の計算が重いケース(大きな配列の生成、同期的なI/O、複雑な計算)です。単純なリテラル(0''false 等)を渡している場合は、遅延初期化にする必要はありません。

なぜ関数を渡すと「初回だけ」実行できるのか

ポイントは2つあります。

1. useStateの初期値は初回レンダーでしか使われない

useStateに渡した初期値が参照されるのは、コンポーネントが最初に画面に表示された1回だけです。2回目以降の再レンダーでは、Reactが内部で保持しているStateの値を使うので、初期値は完全に無視されます。

2. JavaScriptは「関数の引数を先に計算してから関数を呼ぶ」

JavaScriptには「関数を呼び出すとき、カッコの中身(引数)を先に評価してから関数に渡す」というルールがあります。これが落とし穴の原因です。

❌ 値を直接渡した場合

useState(JSON.parse(localStorage.getItem('items') ?? '[]'))

このコードは、レンダーのたびに次の順序で実行されます。

  • localStorage.getItem でデータを取得
  • JSON.parse で文字列をパース
  • その結果を useState に渡す

つまり、useStateが呼ばれる手前の段階で重い処理が完了してしまっています。Reactが「この初期値はもういらない」と判断する余地はありません。結果として、2回目以降のレンダーでも JSON.parse が無駄に実行され続けます。

⭕ 関数を渡した場合

useState(() => JSON.parse(localStorage.getItem('items') ?? '[]'))

このコードでは、useStateに渡しているのは関数そのものです。関数は「定義しただけでは中身が実行されない」ので、useStateにはまだ計算されていない「処理の手順」が渡されます。

そして、Reactは次のように振る舞います。

  • 初回レンダー: 初期値が必要 → 受け取った関数を実行して JSON.parse の結果を取得
  • 2回目以降: Stateは既にあるので初期値は不要 → 受け取った関数は実行せず捨てる

このように「計算をすぐには実行せず、必要なときだけ実行できる形で渡す」テクニックを遅延初期化と呼びます。

Stateの更新

値を直接指定

setCount(10)
setName('田中')
setIsActive(true)

前の値を基に更新

前の値を参照して更新する場合は、関数を渡します。

// 推奨: 関数を使う
setCount(prev => prev + 1)

// 非推奨: 現在の値を直接参照
setCount(count + 1)

このprevは「現在のState値」を受け取る引数です。setCountに関数を渡すと、Reactがその関数に現在の値を渡して実行してくれます。変数名は自由ですが、prev(previous = 前の)がよく使われます。

警告

count のような State 変数は、そのレンダー時点の値に固定されています。同じイベントハンドラ内で何度 setCount を呼んでも、変数 count の値はレンダーが終わるまで変わりません。そのため、直前の更新結果を踏まえて計算したい場合は関数形式を使う必要があります。関数形式で渡された更新は**キュー(順番待ちの処理リスト)**に積まれ、Reactが順番に実行することで、直前の更新結果を次の計算に引き継げます。

// 問題のあるコード
const handleTripleIncrement = () => {
setCount(count + 1) // count = 0 を参照 → 1 を予約
setCount(count + 1) // count は同じレンダーの 0 のまま → 1 を予約
setCount(count + 1) // count は同じレンダーの 0 のまま → 1 を予約
// 3回とも「0 + 1 = 1」を予約するため、最終結果は 1(レンダー内の count は固定値)
}

// 正しいコード
const handleTripleIncrement = () => {
setCount(prev => prev + 1) // キューに積む: 0 → 1
setCount(prev => prev + 1) // キューに積む: 1 → 2
setCount(prev => prev + 1) // キューに積む: 2 → 3
// 結果: 3
}
備考

React は 1 回のイベントハンドラ内で発生した複数の State 更新をまとめて処理し(バッチ処理)、最後に 1 度だけ再レンダリングします。これは React 18 以降、非同期処理の中でも有効です(自動バッチング)。そのため、ハンドラ内で State を読み直しても更新後の値は取得できません。更新後の値を使いたい場合は、関数形式の更新関数か、次のレンダーで受け取った新しい値を使います。

試してみよう: カウンターを作成

+1、-1、リセットボタンを持つカウンターコンポーネントを作ってみましょう。

ヒント
  1. useState(0)で初期値0のStateを作成
  2. 3つのボタンでそれぞれ+1-10に更新
  3. +1と-1はprev => prev + 1のように関数形式で更新
回答と解説
import { useState } from 'react'
import './App.css'

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

// 前の値を基に +1 する(関数形式)
const increment = () => {
setCount(prev => prev + 1)
}

// 前の値を基に -1 する(関数形式)
const decrement = () => {
setCount(prev => prev - 1)
}

// 単純に 0 に設定(値を直接指定)
const reset = () => {
setCount(0)
}

return (
<div>
<p>カウント: {count}</p>
{/* 各ボタンに対応するハンドラを設定 */}
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>リセット</button>
</div>
)
}

export default function App() {
return <Counter />
}

イベントハンドラを関数として定義し、onClickに渡しています。setCount(prev => prev + 1)のように関数形式で更新すると、常に最新の値を基に更新できます。リセットは単純に0を設定するだけなので、関数形式でなくてもOKです。

オブジェクトのState

オブジェクトをStateとして扱う場合、新しいオブジェクトを作成して更新します。※スプレッド構文については02章で解説しています。

警告

オブジェクトや配列のStateは、直接変更(ミューテーション)せず、必ず新しいオブジェクトを作成してください。Reactは「前の値」と「新しい値」の参照(メモリ上の住所)を比較して再レンダリングを判断します。直接変更すると参照が同じままなので、Reactが変更を検知できず画面が更新されません。これは02章で解説した「イミュータブル更新」の原則です。

import { useState } from 'react'
import './App.css'

// Stateで使うオブジェクトの型を定義
type User = {
name: string
age: number
}

function UserForm() {
// オブジェクトを初期値として設定
// <User> で型を明示(推論させることも可能)
const [user, setUser] = useState<User>({ name: '', age: 0 })

// e: イベントオブジェクト(入力要素の情報が入っている)
// React.ChangeEvent<HTMLInputElement> は input 要素の変更イベントの型
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// アロー関数でオブジェクトを返すときは () で囲む
// { ...prev } で既存のプロパティをコピーし、name だけ上書き
setUser(prev => ({ ...prev, name: e.target.value }))
}

const handleAgeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// e.target.value は常に文字列なので、Number() で数値に変換
setUser(prev => ({ ...prev, age: Number(e.target.value) }))
}

return (
<div>
{/* value と onChange で「制御されたコンポーネント」を作成 */}
{/* React が入力値を管理し、State と同期する */}
<input
value={user.name}
onChange={handleNameChange}
placeholder="名前"
/>
<input
type="number"
value={user.age}
onChange={handleAgeChange}
placeholder="年齢"
/>
<p>{user.name}さん({user.age}歳)</p>
</div>
)
}

export default function App() {
return <UserForm />
}

イベントハンドラの引数eにはReact.ChangeEvent<HTMLInputElement>という型を指定しています。これにより、e.target.valueに型安全にアクセスできます。

試してみよう: 入力フォームを作成

名前を入力すると「こんにちは、〇〇さん!」とリアルタイムで表示されるコンポーネントを作ってみましょう。

ヒント
  1. useState('')で空文字の初期値を設定
  2. <input>valueにStateをバインド
  3. onChangeで入力値をStateに反映
  4. {name && <p>...</p>}で名前がある時だけ表示
回答と解説
import { useState } from 'react'
import './App.css'

function GreetingInput() {
// 空文字で初期化(string型と推論される)
const [name, setName] = useState('')

const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// e.target.value で入力された文字列を取得
setName(e.target.value)
}

return (
<div>
{/* value={name} で State の値を表示 */}
{/* onChange={...} で入力のたびに State を更新 */}
<input
type="text"
value={name}
onChange={handleNameChange}
placeholder="名前を入力"
/>
{/* name && ... は「name が空でなければ ... を表示」という意味 */}
{/* 空文字は false 扱いなので、入力があるときだけ表示される */}
{name && <p>こんにちは、{name}さん!</p>}
</div>
)
}

export default function App() {
return <GreetingInput />
}

イベントハンドラを関数として定義し、onChangeに渡しています。valueonChangeを組み合わせて「制御されたコンポーネント」を作ります。{name && ...}で、名前が入力されている時だけ挨拶を表示しています。

配列のState

配列も新しい配列を作成して更新します。

import { useState } from 'react'
import './App.css'

function TodoList() {
// <string[]> で「文字列の配列」型を指定
const [todos, setTodos] = useState<string[]>([])
const [input, setInput] = useState('')

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value)
}

const addTodo = () => {
// trim() で前後の空白を除去し、空でないか確認
if (input.trim()) {
// [...prev, input] で既存の配列に新しい要素を追加した新配列を作成
setTodos(prev => [...prev, input])
// 追加後、入力欄をクリア
setInput('')
}
}

// 引数を受け取るハンドラは「関数を返す関数」で作成
const createRemoveHandler = (index: number) => () => {
// filter で指定した index 以外の要素だけを残す
// _ は「使わない変数」を表す慣習的な書き方
setTodos(prev => prev.filter((_, i) => i !== index))
}

return (
<div>
<input
value={input}
onChange={handleInputChange}
/>
<button onClick={addTodo}>追加</button>
<ul>
{/* map で配列の各要素をリスト項目に変換 */}
{todos.map((todo, index) => (
// key はリストの各要素を識別するために必要
<li key={index}>
{todo}
{/* createRemoveHandler(index) は関数を返すので、onClick に渡せる */}
<button onClick={createRemoveHandler(index)}>削除</button>
</li>
))}
</ul>
</div>
)
}

export default function App() {
return <TodoList />
}
備考

この例では簡略化のためにkey={index}を使用していますが、要素の追加・削除・並び替えがあるリストでは、indexをkeyに使うとバグの原因になります。次の練習問題で、一意のIDを使う正しい方法を学びます。

試してみよう: Todoリストを作成

上記のTodoListコンポーネントを参考に、完了/未完了を切り替えられる機能を追加してみましょう。

ヒント
  1. type Todo = { id: number; text: string; completed: boolean }で型を定義
  2. Date.now()でユニークなIDを生成
  3. <input type="checkbox" checked={...} onChange={...} />で完了状態を操作
  4. mapで配列を変換し、該当IDのcompletedを反転
  5. textDecoration: 'line-through'で取り消し線を表示
回答と解説
import { useState } from 'react'
import './App.css'

// 各Todoの型を定義
type Todo = {
id: number // 一意の識別子
text: string // タスクのテキスト
completed: boolean // 完了状態
}

function TodoList() {
// <Todo[]> で「Todoオブジェクトの配列」型を指定
const [todos, setTodos] = useState<Todo[]>([])
const [input, setInput] = useState('')

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value)
}

const addTodo = () => {
if (input.trim()) {
setTodos(prev => [
...prev,
{
// Date.now() は現在時刻のミリ秒を返す(ユニークなIDとして使用)
id: Date.now(),
text: input,
completed: false
}
])
setInput('')
}
}

// 完了状態を切り替える関数
const toggleTodo = (id: number) => {
setTodos(prev =>
// map で配列を変換(該当する要素だけ変更)
prev.map(todo =>
// 該当IDなら completed を反転、それ以外はそのまま
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
)
}

// 削除する関数
const removeTodo = (id: number) => {
// filter で該当ID以外の要素だけを残す
setTodos(prev => prev.filter(todo => todo.id !== id))
}

return (
<div>
<input
value={input}
onChange={handleInputChange}
placeholder="新しいタスク"
/>
<button onClick={addTodo}>追加</button>
<ul>
{todos.map(todo => (
// key には index ではなく一意のIDを使う(ベストプラクティス)
<li key={todo.id}>
{/* label で囲むとテキストクリックでもチェックが切り替わる */}
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{/* チェックボックスで完了状態を視覚的に表現 */}
<input
type="checkbox"
checked={todo.completed}
// onChange でチェック変更時に toggleTodo を呼び出す
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
// 完了時は取り消し線とグレー色で視覚的に区別
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#999' : 'inherit'
}}>
{todo.text}
</span>
</label>
<button onClick={() => removeTodo(todo.id)}>削除</button>
</li>
))}
</ul>
</div>
)
}

export default function App() {
return <TodoList />
}

チェックボックスのchecked属性にtodo.completedをバインドし、onChangeで状態を切り替えます。<label>で囲むことで、テキストをクリックしてもチェックが切り替わり、操作性が向上します。mapで配列を変換し、該当する要素だけcompletedを反転させます。オブジェクトのスプレッド構文{ ...todo, completed: !todo.completed }で、元のオブジェクトを変更せずに新しいオブジェクトを作成しています。keyにはindexではなく一意のIDを使うのがベストプラクティスです。

複数のState

1つのコンポーネントで複数のStateを持つことができます。

function Form() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)

// ...
}

State vs Props

項目StateProps
管理場所コンポーネント内部親コンポーネント
変更可能可(setState)不可(読み取り専用)
目的内部状態の管理データの受け渡し

Stateのリフトアップ

複数のコンポーネントで同じStateを共有したい場合、共通の親コンポーネントにStateを移動します。

再レンダリングの仕組み

Stateが更新されると、そのコンポーネントと子コンポーネントが再レンダリングされます。

Parent (state変更) → 再レンダリング
├─ Child1 → 再レンダリング
└─ Child2 → 再レンダリング

不要な再レンダリングを防ぐ方法は22章(パフォーマンス最適化)で解説します。

import { useState } from 'react'
import './App.css'

// 表示専用のコンポーネント(Stateを持たず、Propsで値を受け取る)
function Display({ count }: { count: number }) {
return <p>カウント: {count}</p>
}

// 操作専用のコンポーネント(Stateを持たず、Propsで関数を受け取る)
// onIncrement: () => void は「引数なし、戻り値なしの関数」という型
function Controller({ onIncrement }: { onIncrement: () => void }) {
return <button onClick={onIncrement}>+1</button>
}

// 親コンポーネントでStateを管理(リフトアップ)
function Parent() {
// 共有したいStateは親で管理
const [count, setCount] = useState(0)

const handleIncrement = () => {
setCount(prev => prev + 1)
}

return (
<div>
{/* Propsを通じてStateと更新関数を子に渡す */}
<Display count={count} />
<Controller onIncrement={handleIncrement} />
</div>
)
}

export default function App() {
return <Parent />
}

まとめ

  • Stateはコンポーネント内部の変更可能なデータ
  • useStateフックでStateを管理
  • 更新にはsetState関数を使用
  • 前の値を基に更新する場合は関数形式を使う
  • オブジェクト・配列は新しい参照を作成して更新
  • 共有するStateは親にリフトアップ

次の章では、イベントハンドリングについて学びます。

次に読む