コンポーネント設計とデータの流れ
「状態はどこに置くべきか?」という発想
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アプリ で考えます。
やりたいこと:
- テキスト入力して「追加」ボタン
- Todoリストにアイテムが表示される
- 「完了」「削除」などの操作ができる
コンポーネント分割案:
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>
)
}- 単に
todosをmapして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が持つ - その状態と、「状態を変えるための関数」を子に渡す
todos⇒TodoListへhandleAdd⇒TodoFormへhandleToggle,handleDelete⇒TodoList⇒TodoItemへ
データは親が持ち、子は「見た目」と「イベント通知」だけ担当。
これが、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)を意識すると設計しやすい
コメント