[React講座] コンポーネント設計とデータの流れ

当ページのリンクには広告が含まれています。
目次

コンポーネント設計とデータの流れ

「状態はどこに置くべきか?」という発想

Reactでよく出てくるキーワードが、「Single Source of Truth(単一の情報源)」です。

  • 同じ意味の状態を複数の場所で別々に持たない
  • 「このデータの正体はここにある」と1か所に決める

たとえば Todo アプリなら:

  • Todo の配列:[{ id, title, done }, ...]
  • これをあちこちのコンポーネントでバラバラに持たず、「この画面全体のTodoはここで管理する」という親コンポーネントを決める

という考え方になります。

Reactの基本ルール:データは上から下へ

Reactの基本的なデータの流れはとてもシンプルです。

  • 親 ⇒ 子:props(データ)
  • 子 ⇒ 親:props(コールバック関数)

この2つだけで、色々なUIが作れます。

親 ⇒ 子:データを渡す

function Child({ message }) {
  return <p>子コンポーネント: {message}</p>
}

export default function Parent() {
  const msg = 'こんにちは from Parent'
  return <Child message={msg} />
}

重要なのは、「親が持っている値を子が読むだけ」 という一方向性です。

子 ⇒ 親:イベントを伝える(コールバック)

今度は逆向き。
「子コンポーネントでボタンを押したら、親に通知して状態を変えたい」というのは、よくあるパターンです。

ここで使うのが「コールバックprops」。

例:子コンポーネントのボタンで親のカウンターを増やす

// ChildButton.jsx
export default function ChildButton({ onPlus }) {
  return <button onClick={onPlus}>+1(子ボタン)</button>
}
// App.jsx
import { useState } from 'react'
import ChildButton from './ChildButton'

export default function App() {
  const [count, setCount] = useState(0)

  function handlePlus() {
    setCount((prev) => prev + 1)
  }

  return (
    <div>
      <p>カウント: {count}</p>
      <ChildButton onPlus={handlePlus} />
    </div>
  )
}
  • 親は handlePlus という関数を定義
  • それを onPlus という名前で 子に渡す
  • 子は「ボタンが押されたときに onPlus を呼ぶだけ」

子は「イベントが起きたことを親に知らせる」だけで、状態の中身(count が何なのか)は知らなくてよい。

この分離がとても重要です。

状態を「持つコンポーネント」を決める:状態の“持ち上げ”

次に、例として Todoアプリ で考えます。

やりたいこと:

  1. テキスト入力して「追加」ボタン
  2. Todoリストにアイテムが表示される
  3. 「完了」「削除」などの操作ができる

コンポーネント分割案:

  • App:状態をまとめて持つ「親」
  • TodoForm:入力と「追加」ボタン
  • TodoList:Todo一覧
  • TodoItem:1件分の表示+操作

ここでの設計ポイントとして、Todoの配列 state はどこが持つべきか?
⇒ 答えは「App(いちばん上の親)」です。

理由:

  • TodoForm(追加)も
  • TodoList(表示・削除)も

どちらも「同じ Todo 一覧」を見たり更新したりするからです。
2つ以上の子コンポーネントが同じデータを共有するなら、

そのデータを、2つの子の共通の親に“持ち上げる”

というのがReactの定石です。
これを「State Lifting(状態の持ち上げ)」と呼びます。

Todoアプリのコード例(簡易版)

TodoForm.jsx

import { useState } from 'react'

export default function TodoForm({ onAdd }) {
  const [text, setText] = useState('')

  function handleSubmit(e) {
    e.preventDefault()
    if (!text.trim()) return
    onAdd(text.trim())
    setText('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="タスクを入力"
      />
      <button type="submit">追加</button>
    </form>
  )
}
  • text は フォーム内部だけで完結する状態なので、TodoForm が持つ
  • 親に通知したいのは「新しいタスクのテキスト」なので、onAdd(text) を呼ぶだけ

TodoItem.jsx

export default function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li>
      <label>
        <input
          type="checkbox"
          checked={todo.done}
          onChange={() => onToggle(todo.id)}
        />
        <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
          {todo.title}
        </span>
      </label>
      <button onClick={() => onDelete(todo.id)}>削除</button>
    </li>
  )
}
  • todo(1件分のデータ)は props で受け取る
  • 「完了切替」「削除」などのイベントはonToggle(id) / onDelete(id) を呼んで親に任せる

TodoList.jsx

import TodoItem from './TodoItem'

export default function TodoList({ todos, onToggle, onDelete }) {
  if (todos.length === 0) {
    return <p>タスクはありません</p>
  }

  return (
    <ul>
      {todos.map((t) => (
        <TodoItem
          key={t.id}
          todo={t}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  )
}
  • 単に todosmap して TodoItem を並べるだけ
  • 状態は持たず、「表示とイベントの受け渡し」役に徹する

App.jsx

import { useState } from 'react'
import TodoForm from './TodoForm'
import TodoList from './TodoList'

let nextId = 1

export default function App() {
  const [todos, setTodos] = useState([])

  function handleAdd(text) {
    setTodos((prev) => [
      ...prev,
      { id: nextId++, title: text, done: false },
    ])
  }

  function handleToggle(id) {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    )
  }

  function handleDelete(id) {
    setTodos((prev) => prev.filter((todo) => todo.id !== id))
  }

  return (
    <div>
      <h1>Todo アプリ</h1>

      <TodoForm onAdd={handleAdd} />

      <TodoList
        todos={todos}
        onToggle={handleToggle}
        onDelete={handleDelete}
      />
    </div>
  )
}

ここでやっていることを整理すると:

  • 状態(todos)はAppが持つ
  • その状態と、「状態を変えるための関数」を子に渡す
    • todosTodoList
    • handleAddTodoForm
    • handleToggle, handleDeleteTodoListTodoItem

データは親が持ち、子は「見た目」と「イベント通知」だけ担当。

これが、Reactにおける非常に重要な設計パターンです。

Presentational / Container という考え方

さっきのTodoアプリの例は、自然とこんな役割分担になっています:

  • Container(コンテナ)コンポーネント
    • 例:App
    • 状態やロジックを持つ
    • API呼び出しや配列操作、フィルタリングなどの処理担当
  • Presentational(見た目)コンポーネント
    • 例:TodoList, TodoItem
    • props で渡されたデータを表示するだけ
    • クリックや入力の「発生」は通知するが、何をどう更新するかは知らない

この分け方は厳密なルールではなく「設計のコツ」です。

どのコンポーネントが「状態とロジック」を持ち、どのコンポーネントが「見た目」を担当するか?

を意識すると、コードの見通しが良くなります。

Prop Drilling

ここまでの流れで、「親 ⇒ 子 ⇒ 孫 ⇒ ひ孫…と props を延々渡すのはキツそう」という直感が出てくると思います。

これを「Prop Drilling(プロップスを掘りまくる)」と言って、規模が大きくなると問題になります。

この解決策として出てくるのが、

  • React Context
  • グローバルな状態管理(Redux, Zustand など)

ですが、これは後で扱います。
今は 「データは基本、親から子へpropsで落としていく」 という軸だけ理解してください。

まとめ

  • Reactのデータの流れは基本的に 「親 ⇒ 子(props)」の一方向
  • 子 ⇒ 親は 「コールバック関数をpropsで渡す」 ことで実現する
  • 複数のコンポーネントが同じデータを使うなら、一番近い共通の親に state を“持ち上げる”
  • 状態とロジックを持つコンポーネント(Container)と、見た目専用(Presentational)を意識すると設計しやすい

<<前へ(イベント処理と状態管理(useState))

>>次へ(フォームとユーザー入力)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次