Stateの基本:useState
ステートとは、コンポーネントのメモリのことです。プロップが他者から渡されるパラメータ(読み取り専用)であるのに対し、ステートは自分専用の小さなノート(編集可能)のようなものです。useStateは、Reactがコンポーネントに組み込む「メモリチップ」のようなものです。
1. 学習内容
- 「ステート」とは何ですか?また、なぜステートが必要なのでしょうか?
- useState の基本的な使い方
- 配列とオブジェクトの状態を更新する正しい方法(不変性)
- 複数の状態変数の管理
- 州の活性化
2. カウンターが引き起こした考察
(1) 課題:通常の変数ではUIの更新がトリガーされない
ボブは簡単なカウンターを作りたいと思っています。ボタンをクリックすると、数字が1ずつ増えるようなものです。
// ❌ Ordinary Variables,Will not trigger a re-render
function Counter() {
let count = 0 // Ordinary Variables
function handleClick() {
count = count + 1 // Variable changed, but UI won't update!
console.log(count) // Console Display 1, 2, 3... But the page always displays 0
}
return (
<div>
<p>Count:{count}</p> {/* Always Show 0 */}
<button onClick={handleClick}>+1</button>
</div>
)
}
ボブはある問題に気づきました。通常の変数では、ReactにUIを更新するよう通知されないのです。コンポーネントが再レンダリングされるたびに、countは0にリセットされてしまいます。
(2) useState を用いた解決策
// ✅ Usage State,React It automatically tracks changes and updates UI
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0) // Initial value 0
function handleClick() {
setCount(count + 1) // Update Status → React Automatic Re-rendering
}
return (
<div>
<p>Count:{count}</p> {/* Automatically update with each click */}
<button onClick={handleClick}>+1</button>
</div>
)
}
戻り値:useState は配列 [current value, setter function] を返します。setCount を呼び出した後、React は自動的にコンポーネントを再レンダリングし、最新の値を表示します。
3. useState の基本概念
(1) 課題の分解
import { useState } from 'react'
// useState(initialValue) Back [value, setValue]
const [count, setCount] = useState(0)
// ^ ^ ^
// | | └─ Initial value(Applies only to the first render)
// | └─ Update Function(Triggers a re-render after being called)
// └─ Current status value(Get the latest value on every render)
| セクション | 説明 |
|---|---|
count |
現在の状態値。レンダリングのたびに最新の値が取得される |
setCount |
update関数:これを呼び出すと → Reactがコンポーネントを再レンダリングする |
useState(0) |
初期値(最初のレンダリング時に使用され、それ以降のレンダリングでは無視される) |
(2) 更新メカニズム
function Example() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1) // 1. Request Update
// Note:Here count Still the old value!
console.log(count) // Output 0,No 1
}
return <button onClick={handleClick}>{count}</button>
}
重要な概念:
setCount(count + 1)は、「countを即座に変更する」のではなく、「次回のレンダリング時にcountを更新するよう React に指示する」という意味です。これは 非同期更新 と呼ばれます。
(3) 機能の更新
新しい状態が古い状態に依存する場合は、関数型更新を使用する必要があります:
function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
// ✅ Recommendations:Functional Updates
setCount(prev => prev + 1)
setCount(prev => prev + 1) // Called twice in a row, count becomes +2
}
// ❌ Not recommended:Update Directly(Chain calls take effect only once)
function handleBadClick() {
setCount(count + 1)
setCount(count + 1) // Called twice count All are old values,The result was only +1
}
return <button onClick={handleClick}>Currently:{count}</button>
}
| メソッド | 構文 | 特徴 |
|---|---|---|
| 直接更新 | setCount(count + 1) |
シンプルだが、連続して呼び出すとデータが失われる |
| 機能アップデート | setCount(prev => prev + 1) |
正確;連続した呼び出しでも正常に動作する |
4. 配列とオブジェクトの更新(不変性)
React では、状態を直接変更してはなりません。state.push() や state.name = 'xxx' は絶対に使用しないでください。代わりに、新しい配列またはオブジェクトを作成して、古い値と置き換えてください。
(1) 更新対象のオブジェクト
function UserEditor() {
const [user, setUser] = useState({
name: 'Alice',
age: 28,
email: 'alice@example.com'
})
function updateName(newName) {
// ❌ Error:Edit directly state Object
user.name = newName
// React Changes will not be detected,UI Will not be updated
// ✅ Correct:Create a new object
setUser({ ...user, name: newName })
}
function updateAge(newAge) {
// ✅ Expand Operator:Keep the other fields,Update Only age
setUser({ ...user, age: newAge })
}
function resetUser() {
// ✅ Reset to default values
setUser({ name: '', age: 0, email: '' })
}
return (
<div>
<p>{user.name} - {user.age} years old</p>
<button onClick={() => updateName('Bob')}>Change Name Bob</button>
<button onClick={() => updateAge(user.age + 1)}>Age +1</button>
</div>
)
}
(2) 配列を更新する
function ShoppingCart() {
const [items, setItems] = useState([
{ id: 1, name: 'Apple', qty: 2 },
{ id: 2, name: 'Banana', qty: 1 }
])
// Add:Create a new array using the spread operator
function addItem(name) {
setItems([...items, { id: Date.now(), name, qty: 1 }])
}
// Delete:use filter Create a new array
function removeItem(id) {
setItems(items.filter(item => item.id !== id))
}
// Update:use map Create a new array
function updateQty(id, newQty) {
setItems(items.map(item =>
item.id === id ? { ...item, qty: newQty } : item
))
}
return (
<div>
<button onClick={() => addItem('Orange')}>Add orange</button>
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} × {item.qty}
<button onClick={() => updateQty(item.id, item.qty + 1)}>+</button>
<button onClick={() => removeItem(item.id)}>Delete</button>
</li>
))}
</ul>
</div>
)
}
(1) ▶ サンプル:配列演算のクイックリファレンス表
// ============================================
// Example:React Array Operations in C 6 Common Scenarios
// ============================================
const [arr, setArr] = useState([1, 2, 3])
// 1. Append to the end
setArr([...arr, 4]) // [1, 2, 3, 4]
// 2. Add to the beginning
setArr([0, ...arr]) // [0, 1, 2, 3]
// 3. Insert in the middle
const insertAt = 1
setArr([...arr.slice(0, insertAt), 99, ...arr.slice(insertAt)])
// [1, 99, 2, 3]
// 4. Delete Element(filter)
setArr(arr.filter(n => n !== 2)) // [1, 3]
// 5. Update Element(map)
setArr(arr.map(n => n === 2 ? 22 : n)) // [1, 22, 3]
// 6. Sort(Copy first, then sort)
setArr([...arr].sort((a, b) => b - a)) // [3, 2, 1]
5. 複数の状態変数
コンポーネントには複数の状態変数を設定できます。それらを1つの巨大なオブジェクトに詰め込むのではなく、ロジックごとに分割することをお勧めします:
function RegistrationForm() {
// ✅ Recommendations:Logical Breakdown
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [errors, setErrors] = useState({})
// ❌ Not recommended:All states are stored in a single object
const [form, setForm] = useState({
name: '', email: '', password: '',
isSubmitting: false, errors: {}
})
// Expand the entire object when updating,Fields that are easy to overlook
}
| 戦略 | メリット | デメリット |
|---|---|---|
複数の useState に分割する |
更新が正確で、型安全、かつ読みやすい | 状態が多いと変数が多くなりすぎる |
1つのuseStateにまとめられている |
複数のフィールドを一度に更新するのに便利 | 更新の構文が煩雑で、展開し忘れることがよくある |
推奨事項:独立した状態(入力フィールド、読み込みインジケーター、エラーメッセージなど)には複数の
useStateインスタンスを使用してください。論理的に関連するフィールド(ユーザープロファイル内のすべてのフィールドなど)は、1つのオブジェクトにまとめることができます。
6. 状態の引き上げ
複数のコンポーネントが同じ状態を共有する必要がある場合は、その状態を、それらに最も近い共通の親コンポーネントへと引き上げてください。
graph TB
subgraph "Before the status upgrade(Data is not shared)"
A[Parent Component App] --- B[Child component A<br/>Have their own count]
A --- C[Child component B<br/>Have their own count]
end
subgraph "After the status upgrade(Data Sharing)"
D[Parent Component App<br/>count Here] --- E[Child component A<br/>Read count, Call setCount]
D --- F[Child component B<br/>Read count, Call setCount]
end
(1) ▶ サンプル:州の振興策の実践的な活用
// ============================================
// Example:Temperature Converter(Status Upgrade)
// Features:Converting Between Celsius and Fahrenheit,Share the same temperature value
// ============================================
// ---- Child component:Celsius Temperature Input ----
function CelsiusInput({ celsius, onCelsiusChange }) {
return (
<div>
<label>Celsius(°C):</label>
<input
value={celsius}
onChange={e => onCelsiusChange(e.target.value)}
style={{ margin: '8px', padding: '4px' }}
/>
</div>
)
}
// ---- Child component:Fahrenheit Temperature Input ----
function FahrenheitInput({ fahrenheit, onFahrenheitChange }) {
return (
<div>
<label>Fahrenheit(°F):</label>
<input
value={fahrenheit}
onChange={e => onFahrenheitChange(e.target.value)}
style={{ margin: '8px', padding: '4px' }}
/>
</div>
)
}
// ---- Parent Component:Status is managed here ----
function TemperatureConverter() {
// State Propagated to the Common Parent Component
const [temperature, setTemperature] = useState('')
function handleCelsiusChange(value) {
setTemperature(value) // Store at Celsius
// There is no need for two states!Keep only one,Another one calculated using a formula
}
function handleFahrenheitChange(value) {
// Fahrenheit → Celsius:°C = (°F - 32) × 5/9
setTemperature(value ? ((parseFloat(value) - 32) * 5 / 9).toFixed(1) : '')
}
const celsius = temperature
const fahrenheit = temperature
? (parseFloat(temperature) * 9 / 5 + 32).toFixed(1)
: ''
return (
<div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
<h2>Temperature Converter</h2>
<CelsiusInput celsius={celsius} onCelsiusChange={handleCelsiusChange} />
<FahrenheitInput fahrenheit={fahrenheit} onFahrenheitChange={handleFahrenheitChange} />
{temperature && (
<p style={{ color: '#666', marginTop: '12px' }}>
{celsius}°C = {fahrenheit}°F
</p>
)}
</div>
)
}
// Enter in the Celsius input field 100 → Automatic Display in Fahrenheit 212°F
// Enter in the Fahrenheit input field 212 → Automatic Celsius Display 100°C
7. 完全な例:ショッピングカートのカウンター
// ============================================
// Complete Example:Shopping Cart(useState Comprehensive Application)
// Features:Add Item、Increase or Decrease Quantity、Delete、Total Price Calculation
// ============================================
import { useState } from 'react'
function ShoppingCart() {
// Several State Variable,Logical Breakdown
const [items, setItems] = useState([
{ id: 1, name: 'React Hands-On Tutorials', price: 89, qty: 1 },
{ id: 2, name: 'TypeScript Getting Started', price: 59, qty: 2 }
])
const [discountCode, setDiscountCode] = useState('')
const [appliedDiscount, setAppliedDiscount] = useState(0)
// Increase the quantity(Functional updates ensure accuracy)
function increment(id) {
setItems(items.map(item =>
item.id === id ? { ...item, qty: item.qty + 1 } : item
))
}
// Decrease the quantity(No less than 1)
function decrement(id) {
setItems(items.map(item =>
item.id === id ? { ...item, qty: Math.max(1, item.qty - 1) } : item
))
}
// Delete Product
function remove(id) {
setItems(items.filter(item => item.id !== id))
}
// Apply a discount code
function applyDiscount() {
if (discountCode === 'REACT2026') {
setAppliedDiscount(20) // over 100 minus 20
} else {
alert('The discount code is invalid.')
}
}
// Calculate the Total Price
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0)
const total = Math.max(0, subtotal - appliedDiscount)
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<h2>🛒 Shopping Cart</h2>
{/* Product List */}
{items.length === 0 ? (
<p style={{ color: '#999', textAlign: 'center', padding: '40px' }}>
Your shopping cart is empty,Go check it out!!
</p>
) : (
items.map(item => (
<div key={item.id} style={{
display: 'flex', alignItems: 'center',
padding: '12px', borderBottom: '1px solid #f0f0f0'
}}>
<div style={{ flex: 1 }}>
<h4 style={{ margin: 0 }}>{item.name}</h4>
<p style={{ margin: '4px 0', color: '#ff4d4f' }}>${item.price}</p>
</div>
{/* Quantity Control */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button onClick={() => decrement(item.id)} style={btnStyle}>−</button>
<span>{item.qty}</span>
<button onClick={() => increment(item.id)} style={btnStyle}>+</button>
</div>
{/* Subtotal */}
<p style={{ margin: '0 16px', fontWeight: 'bold', width: '80px', textAlign: 'right' }}>
${item.price * item.qty}
</p>
{/* Delete */}
<button onClick={() => remove(item.id)} style={{ ...btnStyle, backgroundColor: '#ff4d4f', color: 'white' }}>
✕
</button>
</div>
))
)}
{/* Discount Code */}
<div style={{ marginTop: '16px', display: 'flex', gap: '8px' }}>
<input
value={discountCode}
onChange={e => setDiscountCode(e.target.value)}
placeholder="Enter the discount code"
style={{ flex: 1, padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}
/>
<button onClick={applyDiscount} style={{
padding: '8px 16px', backgroundColor: '#52c41a', color: 'white',
border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Applications
</button>
</div>
{/* Price Summary */}
<div style={{ marginTop: '16px', padding: '16px', backgroundColor: '#fafafa', borderRadius: '8px' }}>
<p>Subtotal:${subtotal}</p>
{appliedDiscount > 0 && <p style={{ color: '#52c41a' }}>Discount: -${appliedDiscount}</p>}
<p style={{ fontSize: '20px', fontWeight: 'bold' }}>Total:${total}</p>
</div>
</div>
)
}
const btnStyle = {
width: '32px', height: '32px',
border: '1px solid #d9d9d9', borderRadius: '4px',
backgroundColor: 'white', cursor: 'pointer',
fontSize: '16px', display: 'flex', alignItems: 'center',
justifyContent: 'center'
}
インタラクションの流れ:
- 「+/−」をタップして数量を調整すると、小計と合計がリアルタイムで更新されます
- ✕をクリックして商品を削除する。ショッピングカートが空のときは、わかりやすいメッセージを表示する
- プロモーションコード
REACT2026を入力すると、20元割引になります
(1) ▶ サンプル:オブジェクトの状態の更新—ユーザー情報の編集
function UserEditor() {
const [user, setUser] = useState({ name: 'Alice', email: 'alice@test.com', age: 28 })
function updateField(field, value) {
setUser(prev => ({ ...prev, [field]: value }))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Edit Profile</h3>
<input value={user.name} onChange={e => updateField('name', e.target.value)}
placeholder="Name" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<input value={user.email} onChange={e => updateField('email', e.target.value)}
placeholder="Email" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<input type="number" value={user.age} onChange={e => updateField('age', Number(e.target.value))}
placeholder="Age" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4, fontSize: 13 }}>
{JSON.stringify(user, null, 2)}
</pre>
</div>
)
}
(2) ▶ サンプル:ネストされたオブジェクトの状態 — 住所管理
function AddressForm() {
const [address, setAddress] = useState({
street: '', city: '', zip: '',
country: 'US', isPrimary: true,
})
function update(path, value) {
setAddress(prev => ({ ...prev, [path]: value }))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Shipping Address</h3>
<input value={address.street} onChange={e => update('street', e.target.value)}
placeholder="Street" style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input value={address.city} onChange={e => update('city', e.target.value)}
placeholder="City" style={{ flex: 2, padding: 8, borderRadius: 4 }} />
<input value={address.zip} onChange={e => update('zip', e.target.value)}
placeholder="ZIP" style={{ flex: 1, padding: 8, borderRadius: 4 }} />
</div>
<select value={address.country} onChange={e => update('country', e.target.value)}
style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }}>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="JP">Japan</option>
</select>
<label>
<input type="checkbox" checked={address.isPrimary}
onChange={e => update('isPrimary', e.target.checked)} /> Primary address
</label>
</div>
)
}
(3) ▶ サンプル:関数による更新でバッチ更新の問題を解決する
function ScoreBoard() {
const [score, setScore] = useState(0)
const [multiplier, setMultiplier] = useState(1)
function addPoints(base) {
setScore(prev => prev + base * multiplier)
}
function resetScore() {
setScore(0)
setMultiplier(1)
}
function doubleMultiplier() {
setMultiplier(prev => Math.min(prev * 2, 8))
}
return (
<div style={{ maxWidth: 300, margin: '0 auto', textAlign: 'center' }}>
<h3>Score: {score}</h3>
<p>Multiplier: x{multiplier}</p>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', marginBottom: 8 }}>
<button onClick={() => addPoints(10)} style={{ padding: '8px 16px', cursor: 'pointer' }}>+10 pts</button>
<button onClick={() => addPoints(50)} style={{ padding: '8px 16px', cursor: 'pointer' }}>+50 pts</button>
<button onClick={() => addPoints(100)} style={{ padding: '8px 16px', cursor: 'pointer' }}>+100 pts</button>
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
<button onClick={doubleMultiplier} style={{ padding: '8px 16px', cursor: 'pointer' }}>2x Multiplier</button>
<button onClick={resetScore} style={{ padding: '8px 16px', cursor: 'pointer' }}>Reset</button>
</div>
</div>
)
}
❓ よくある質問
useStateと通常の変数では、パフォーマンスにどのような違いがありますか?useStateは追加のオーバーヘッド(変更の追跡、レンダリングのスケジューリング)を伴うため、「変更が発生した際にUIの更新がトリガーされる」ようなシナリオでのみ使用すべきです。変数が変更されても UI の更新を必要としない場合(タイマー ID やスクロール位置など)、useRef(第 10 課)を使用する方が適切です。useStateをいくつまで含めることができますか?パフォーマンス上の問題はありますか?useStateに分割することを推奨しています。各useStateの呼び出しは「状態単位」であり、Reactは多数のuseStateを効率的に処理できます。一般的なコンポーネントには、通常3~8個のuseStateが含まれます。setState の更新は同期的ですか、それとも非同期的ですか?setState はイベントハンドラ内では「自動的にバッチ処理」されていましたが、setTimeout および Promise の呼び出し内では同期的に実行されていました。React 18では「自動バッチ処理」が導入されたため、setStateはイベントハンドラ、setTimeout、Promise、fetchのコールバック内で呼び出されるかどうかにかかわらず、非同期バッチ処理されるようになりました。更新されたDOMに同期的にアクセスする必要がある場合は、flushSync(() => setState(...))を使用できます。📖 まとめ
- ステートはコンポーネントの「メモリ」のようなもので、これが変化すると、自動的に UI の更新がトリガーされます
useState(initialValue)は[value, setValue]配列を返します- 不変性:状態を直接変更してはならない。代わりに、新しいオブジェクトや配列を作成して、それを置き換えること。
- 機能アップデート
setCount(prev => prev + 1)は、以前の値に依存する場合、より正確になります - 状態の昇格:複数の子コンポーネントでデータを共有する必要がある場合、その状態は共通の親コンポーネントに昇格されます。
📝 練習問題
- 基本演習(難易度 ⭐):クリックすると ❤️ と 🤍 の状態を切り替え、いいねの数を表示する
LikeButtonコンポーネントを作成してください。 - 上級演習(難易度 ⭐⭐):
useStateを使用して経費のリスト(説明、金額、日付)を管理し、項目の追加や削除に対応するExpenseTrackerコンポーネントを作成してください。 - 課題(難易度:⭐⭐⭐):3つの
useStateインスタンスを使用してR、G、Bのスライダー値(0~255)を管理し、リアルタイムの色プレビューと16進数のカラーコードを表示するColorPickerコンポーネントを作成してください。



