404 Not Found

404 Not Found


nginx

Stateの基本:useState

ステートとは、コンポーネントのメモリのことです。プロップが他者から渡されるパラメータ(読み取り専用)であるのに対し、ステートは自分専用の小さなノート(編集可能)のようなものです。useStateは、Reactがコンポーネントに組み込む「メモリチップ」のようなものです。


1. 学習内容



2. カウンターが引き起こした考察

(1) 課題:通常の変数ではUIの更新がトリガーされない

ボブは簡単なカウンターを作りたいと思っています。ボタンをクリックすると、数字が1ずつ増えるようなものです。

JSX
// ❌ 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 を用いた解決策

JSX
// ✅ 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) 課題の分解

JSX
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) 更新メカニズム

JSX
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) 機能の更新

新しい状態が古い状態に依存する場合は、関数型更新を使用する必要があります:

JSX
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) 更新対象のオブジェクト

JSX
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) 配列を更新する

JSX
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) ▶ サンプル:配列演算のクイックリファレンス表

JSX
// ============================================
// 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つの巨大なオブジェクトに詰め込むのではなく、ロジックごとに分割することをお勧めします

JSX
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. 状態の引き上げ

複数のコンポーネントが同じ状態を共有する必要がある場合は、その状態を、それらに最も近い共通の親コンポーネントへと引き上げてください。

100%
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) ▶ サンプル:州の振興策の実践的な活用

JSX
// ============================================
// 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. 完全な例:ショッピングカートのカウンター

JSX
// ============================================
// 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) ▶ サンプル:オブジェクトの状態の更新—ユーザー情報の編集

JSX
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) ▶ サンプル:ネストされたオブジェクトの状態 — 住所管理

JSX
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) ▶ サンプル:関数による更新でバッチ更新の問題を解決する

JSX
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>
  )
}
▶ 試してみよう

❓ よくある質問

Q useStateと通常の変数では、パフォーマンスにどのような違いがありますか?
A useStateは追加のオーバーヘッド(変更の追跡、レンダリングのスケジューリング)を伴うため、「変更が発生した際にUIの更新がトリガーされる」ようなシナリオでのみ使用すべきです。変数が変更されても UI の更新を必要としない場合(タイマー ID やスクロール位置など)、useRef(第 10 課)を使用する方が適切です。
Q 1つのコンポーネント内にuseStateをいくつまで含めることができますか?パフォーマンス上の問題はありますか?
A 数に制限はありません。Reactの公式ドキュメントでは、1つの大きなオブジェクトを使用するのではなく、ロジックに基づいて複数のuseStateに分割することを推奨しています。各useStateの呼び出しは「状態単位」であり、Reactは多数のuseStateを効率的に処理できます。一般的なコンポーネントには、通常3~8個のuseStateが含まれます。
Q setState の更新は同期的ですか、それとも非同期的ですか?
A React 18 以前では、setState はイベントハンドラ内では「自動的にバッチ処理」されていましたが、setTimeout および Promise の呼び出し内では同期的に実行されていました。React 18では「自動バッチ処理」が導入されたため、setStateはイベントハンドラ、setTimeout、Promise、fetchのコールバック内で呼び出されるかどうかにかかわらず、非同期バッチ処理されるようになりました。更新されたDOMに同期的にアクセスする必要がある場合は、flushSync(() => setState(...))を使用できます。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):クリックすると ❤️ と 🤍 の状態を切り替え、いいねの数を表示する LikeButton コンポーネントを作成してください。
  2. 上級演習(難易度 ⭐⭐)useState を使用して経費のリスト(説明、金額、日付)を管理し、項目の追加や削除に対応する ExpenseTracker コンポーネントを作成してください。
  3. 課題(難易度:⭐⭐⭐):3つのuseStateインスタンスを使用してR、G、Bのスライダー値(0~255)を管理し、リアルタイムの色プレビューと16進数のカラーコードを表示するColorPickerコンポーネントを作成してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%