リアクティブデータ — useState

useState は React で最も重要な Hook です。この章では React の状態管理の仕組みを理解し、カテゴリ絞り込み機能を実装します。


Reactの「リアクティブ」とVue3の違いは?

Vue3 では ref/reactive を使ってリアクティブデータを作成し、データが変わるとビューが自動的に更新されます。

React の発想は異なります:state を変更すると、React は新しい state でコンポーネント関数を再実行し、新しいビューをレンダリングします。。

これが React という名前の由来です——コンポーネントが state の変化に「反応」します。


useState 基本の使い方

useStateこれは React で最も基本的な Hook で、関数コンポーネントに状態を追加するために使います。

実例

// react から分割代入で useState をインポート
import { useState } from 'react'

function Counter() {
  // useState(初期値) は配列を返します: [現在の値, 更新関数]
  const [count, setCount] = useState(0)

  function handleClick() {
    setCount(count + 1)    // 状態を更新すると、React が自動的にコンポーネントを再レンダリングします
  }

  return (
    <div>
      <p>クリック回数:{count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  )
}

ボタンをクリックするたびに、setCountcount の値を更新すると、React は新しい値で Counter 関数を再実行し、新しい JSX を返すため、ページが自動的に更新されます。

React の規約:状態名には[something, setSomething]分割代入を使います。set で始まる関数は慣習的な命名規則であり、必須ではありませんが、コミュニティ全体がこのように書いています。


なぜstateを直接変更できないのか?

React では、state は読み取り専用です。setter 関数を通して更新する必要があり、直接変更することはできません。

実例

import { useState } from 'react'

function ArticleManager() {
  const [title, setTitle] = useState('默认标题')

  // 間違った書き方:直接変更
  // title = '新标题' // React は変更に気づかないため、再レンダリングされません

  // 正しい書き方:setter で更新
  function updateTitle() {
    setTitle('新标题')  // React は状態が変わったことを認識し、再レンダリングします
  }

  return (
    <div>
      <p>{title}</p>
      <button onClick={updateTitle}>タイトルを変更</button>
    </div>
  )
}

この背景には React の不変(Immutable)原則があります:古いデータを変更するのではなく、新しいデータで置き換えます。


配列とオブジェクトを更新する正しい方法

配列とオブジェクトについては、直接 push、splice、またはプロパティの変更はできません。新しいコピーを作成する必要があります。

実例

import { useState } from 'react'

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React', done: false },
    { id: 2, text: '写博客', done: true }
  ])

  // 項目を追加:古い配列を展開 + 新しい要素
  function addTodo(text) {
    const newTodo = { id: Date.now(), text, done: false }
    setTodos([...todos, newTodo])        // スプレッド演算子で新しい配列を作成
  }

  // 項目を修正:map で走査し、変更対象を見つけて新しいオブジェクトを返す
  function toggleTodo(id) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ))
  }

  // 項目を削除:filter で不要なものを取り除く
  function removeTodo(id) {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
          {todo.done ? <s>{todo.text}</s> : todo.text}
        </li>
      ))}
    </ul>
  )
}
操作間違った書き方正しい書き方
追加arr.push(item)setArr([...arr, item])
修正arr[0] = newValsetArr(arr.map(el => el.id === id ? ... : el))
削除arr.splice(i, 1)setArr(arr.filter(el => el.id !== id))
オブジェクトのプロパティ変更obj.name = 'xxx'setObj({ ...obj, name: 'xxx' })

核心原則:常に新しい配列/新しいオブジェクトで古いものを置き換える。スプレッド演算子...と学んだ配列メソッド(map、filter)は、不変性更新の最良のパートナーです。


useMemo — 派生計算値

useMemo計算結果をキャッシュするために使います。依存関係が変わらないときは再計算をスキップし、不要なパフォーマンスコストを回避します。

実例

import { useState, useMemo } from 'react'

function ArticleFilter() {
  const [articles] = useState([
    { id: 1, title: 'React 入门', category: 'React' },
    { id: 2, title: 'Promise 详解', category: 'JavaScript' },
    { id: 3, title: 'Grid 布局', category: 'CSS' },
    { id: 4, title: 'React Hooks', category: 'React' }
  ])
  const [activeCategory, setActiveCategory] = useState('全部')

  // useMemo がフィルタ結果をキャッシュし、依存関係が変わったときだけ再計算
  const filteredArticles = useMemo(() => {
    console.log('重新计算过滤结果')  // キャッシュ効果を検証
    if (activeCategory === '全部') return articles
    return articles.filter(a => a.category === activeCategory)
  }, [articles, activeCategory])  // 依存配列

  // useMemo で統計データを計算
  const totalCount = useMemo(() => articles.length, [articles])
  const filteredCount = useMemo(() => filteredArticles.length, [filteredArticles])

  return (
    <div>
      <p>共 {totalCount}件、現在{filteredCount} 篇</p>
      <button onClick={() => setActiveCategory('全部')}>すべて</button>
      <button onClick={() => setActiveCategory('React')}>React</button>
      <button onClick={() => setActiveCategory('JavaScript')}>JavaScript</button>
      <button onClick={() => setActiveCategory('CSS')}>CSS</button>

      {filteredArticles.map(a => (
        <div key={a.id}><h3>{a.title}</h3><span>{a.category}</span></div>
      ))}
    </div>
  )
}

useMemo(fn, deps) の依存配列は完全に記入する必要があります:関数内で使用するすべてのコンポーネント内部変数を配列に宣言する必要があります。React は浅い比較によって再計算するかどうかを決定します。


実践:ブログにカテゴリ絞り込みを追加する

実例

// ファイルパス:src/App.jsx
import { useState, useMemo } from 'react'
import './App.css'

function App() {
  const [articles] = useState([
    { id: 1, title: 'React 入门完全指南', summary: 'ゼロから React を学ぶ', category: 'React', date: '2024-05-10' },
    { id: 2, title: 'JS 异步编程详解', summary: 'Promise と async/await を理解する', category: 'JavaScript', date: '2024-05-08' },
    { id: 3, title: 'CSS Grid 布局实战', summary: '用 Grid 实现响应式布局', category: 'CSS', date: '2024-05-05' },
    { id: 4, title: 'React Hooks 深入', summary: 'useState と useEffect を深く理解する', category: 'React', date: '2024-05-03' },
    { id: 5, title: 'Flexbox 完全指南', summary: '一文学会弹性布局', category: 'CSS', date: '2024-05-01' },
  ])

  const [activeCategory, setActiveCategory] = useState('全部')

  // すべてのカテゴリを抽出(重複除去)
  const categories = useMemo(() => {
    const cats = articles.map(a => a.category)
    return ['全部', ...new Set(cats)]
  }, [articles])

  // カテゴリに基づいて記事をフィルタリング
  const filteredArticles = useMemo(() => {
    if (activeCategory === '全部') return articles
    return articles.filter(a => a.category === activeCategory)
  }, [articles, activeCategory])

  return (
    <div className="app">
      <header className="navbar">
        <h1 className="logo">サンプル Blog</h1>
        <nav>
          <a href="/">ホーム</a>
          <a href="#">概要</a>
        </nav>
      </header>

      <main className="container">
        <h2 className="section-title">最新記事</h2>

        {/* カテゴリ絞り込みボタングループ */}
        <div className="category-bar">
          {categories.map(cat => (
            <button
              key={cat}
              className={activeCategory === cat ? 'active' : ''}
              onClick={() => setActiveCategory(cat)}
            >
              {cat}
            </button>
          ))}
        </div>

        <p className="result-info">共 {filteredArticles.length} 篇</p>

        {filteredArticles.length === 0 ? (
          <p className="empty-tip">このカテゴリにはまだ記事がありません</p>
        ) : (
          <div className="article-grid">
            {filteredArticles.map(article => (
              <div key={article.id} className="article-card">
                <div className="card-content">
                  <span className="card-category">{article.category}</span>
                  <h3>{article.title}</h3>
                  <p>{article.summary}</p>
                  <span className="card-date">{article.date}</span>
                </div>
              </div>
            ))}
          </div>
        )}
      </main>

      <footer className="footer">
        <p>© 2024 サンプル Blog. Powered by React.</p>
      </footer>
    </div>
  )
}

export default App

実例

/* ファイルパス:src/App.css カテゴリ絞り込みボタンのスタイルを追加 */
.category-bar {
  display: flex;
  gap: 10px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.category-bar button {
  padding: 6px 16px;
  border: 1px solid #ddd;
  border-radius: 20px;
  background: #fff;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.2s;
}

.category-bar button.active {
  background: #1976d2;
  color: #fff;
  border-color: #1976d2;
}

.category-bar button:hover {
  border-color: #1976d2;
}

.result-info {
  color: #999;
  font-size: 14px;
  margin-bottom: 16px;
}

本章のまとめ

この章では、React の状態管理の3つの核心概念を習得しました:useState による状態の作成と更新、不変更新の原則(スプレッド演算子 + map/filter)、useMemo による派生計算値のキャッシュです。

カテゴリ絞り込み機能を通じて、React の「setState → 再レンダリング → 新しいビュー」というデータ駆動パターンを体験しました。

その他の拡張