3.5 Props と単方向データフロー
ここで理解してほしいのは、「props は親から子への 読み取り専用の引数 であり、データは常に親 → 子の一方向にしか流れない」という一点です。この制約こそが、React コードの見通しを大きく支えているんですね。
最初は少し堅苦しく感じる制約ですが、慣れてくると「読むときにどこを見ればいいか」が一気に絞れるようになります。
props とは何か
コンポーネントは関数です。関数に引数があるように、コンポーネントにも引数があります。React ではこれを props と呼びます。
Props are the information that you pass to a JSX tag. ... They serve the same role as arguments serve for functions—in fact, props are the only argument to your component!
JSX の属性のように見える記法で親から子に値を渡し、子は関数の引数として受け取ります。
// 子
type GreetingProps = {
name: string
count: number
}
function Greeting({ name, count }: GreetingProps) {
return <p>{name} さん、{count} 回目の訪問です</p>
}
// 親
export default function App() {
return <Greeting name="yuichkun" count={3} />
}文字列は "..."、それ以外の値 (数値、真偽値、オブジェクト、関数など) は {...} で渡します。
props は読み取り専用
React は props のイミュータビリティ (不変性) を強く保証します。
Props are immutable—a term from computer science meaning "unchangeable". When a component needs to change its props (for example, in response to a user interaction or new data), it will have to "ask" its parent component to pass it different props—a new object!
出典: 同上
さらに核となる一文はこれです。
Props are read-only snapshots in time: every render receives a new version of props. You can't change props. When you need interactivity, you'll need to set state.
出典: 同上
子の側で props.name = 'other' のように書き換えても、React は何もしてくれません。値を変えたいときは 親の state を変える 必要があります (後述)。
単方向データフロー
React のデータは常に親から子へ流れます。これを 単方向データフロー と呼びます。
図 3.5.1 親から子への流れ
state が変わるのは「そのコンポーネント自身」または「祖先」だけです。子から親に値を戻したいときは、親からコールバック関数を props として渡し、子がそれを呼ぶ という連携に変えます (次節)。
この制約があるからこそ、「画面のどこを見れば何が変わるか」を機械的に辿れます。Vanilla JS で起きていた「どこから DOM を触ったか分からない」問題 (Part 2) が、React では構造的に起こりにくくなっています。
親のボタンを押すと子に値が流れる様子
文章で書くと抽象的なので、実際に触って確かめてみましょう。下のデモは「親が count という state を持っていて、それを props として子に渡している」だけの最小例です。親のボタンを押すと、新しい値が線を伝って子に届く様子が見えます。
count = 0count = 0子は props を読むだけ。書き換える手段はありません。
注目してほしいのは次の 2 点です。
- 親のボタンだけが count を変えられる — 子には変える手段がそもそも用意されていない
- 値は毎回「新しく」流れる — 子が props を書き換えているわけではなく、親が新しい値を渡し直している
この「流れ方」が染みつくと、後の章で useState や useEffect が出てきても、データの動きを追いやすくなります。
TypeScript で props の型を書く
props の型は type または interface で定義します。関数の引数に分割代入と型注釈を書くのが一般的です。
// 方法 1: 別名で型を定義
type ButtonProps = {
label: string
onClick: () => void
disabled?: boolean // ? で省略可能
}
function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button type="button" onClick={onClick} disabled={disabled}>
{label}
</button>
)
}デフォルト値
分割代入の際に = を書くと、未指定時 (または undefined の時) にデフォルト値が使われます。
The default value is only used if the
sizeprop is missing or if you passsize={undefined}. But if you passsize={null}orsize={0}, the default value will not be used.
つまり null や 0 は「明示的にその値を指定した」と解釈されます。
スプレッド構文
親から受け取った props をそのまま子に渡したいときは、スプレッド (...) が使えます。
function Wrapper(props: ButtonProps) {
return <Button {...props} />
}ただし公式は節度ある使用を勧めています。
Use spread syntax with restraint. If you're using it in every other component, something is wrong.
出典: 同上
どの props を渡しているか読めなくなりやすいので、明示的に書くのが基本です。
children で「中身」を受け取る
コンポーネントで囲んだ内容は、自動的に children という props として渡ります。
type CardProps = {
title: string
children: React.ReactNode // React がレンダーできる値の型
}
function Card({ title, children }: CardProps) {
return (
<section className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</section>
)
}
// 使う側
<Card title="お知らせ">
<p>新機能を追加しました</p>
<button type="button">閉じる</button>
</Card>公式は children をこう説明しています。
When you nest content inside a JSX tag, the parent component will receive that content in a prop called
children.出典: 同上
Card の実装は「外枠 (見出し + div) だけを提供し、中身は呼び出し側が決める」という構造になっています。レイアウト用コンポーネントや、モーダル、カード、リストアイテムなど、囲む系 のコンポーネントで頻出のパターンです。
state のリフトアップ — 兄弟間でデータを共有する
2 つの子コンポーネントが同じ値を共有したくなる場面があります。props は親から子にしか流れないので、共通の親に state を持たせる 必要があります。これを state のリフトアップ (lifting state up) と呼びます。
Instead of duplicating shared state between components, lift it up to their common shared parent, and pass it down to the children that need it.
例: 検索ボックスと結果カウント
検索ボックスとカウント表示は、同じ文字列を共有する必要があります。
import { useState } from 'react'
type SearchBoxProps = {
value: string
onChange: (next: string) => void
}
function SearchBox({ value, onChange }: SearchBoxProps) {
return (
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)
}
type CountProps = {
query: string
items: string[]
}
function Count({ query, items }: CountProps) {
const matched = items.filter((x) => x.includes(query)).length
return <p>{matched} 件該当</p>
}
// 共通の親が state を持つ
export default function App() {
const [query, setQuery] = useState('')
const items = ['apple', 'banana', 'grape']
return (
<>
<SearchBox value={query} onChange={setQuery} />
<Count query={query} items={items} />
</>
)
}ここで起きていることを確認します。
queryの state は 共通の親Appが持つ。SearchBoxはqueryを props として受け取り、変更したいときは props で渡されたonChangeを呼ぶ (子 → 親の「お願い」)。Countもqueryを props として受け取り、ただ読むだけ。
図 3.5.2 リフトアップの構造
点線の「お願い」は関数呼び出しにすぎません。props で渡されたコールバックを呼ぶだけです。データ自体は常に App → 子の方向にしか流れません。
リフトアップの 3 ステップ
公式はこう整理しています。
- Remove state from the child components.
- Pass hardcoded data from the common parent.
- Add state to the common parent and pass it down together with the event handlers.
出典: 同上
迷ったらこの順序で動かすと安全です。
まとめ
- props は親から子への引数。読み取り専用。
- データは常に親 → 子の単方向に流れる。
- 子から親に情報を戻したいときは、コールバックを props として渡す。
- 複数の子でデータを共有したいときは、共通の親に state を持たせる (リフトアップ)。
childrenは「中に入れた内容」を受け取る特別な props。
さらに深く学ぶには
- react.dev — Passing Props to a Component — 完全なリファレンス。
- react.dev — Sharing State Between Components — リフトアップの詳細と演習。
- react.dev — Thinking in React — 画面を props と state にどう分解するかの思考法。