Skip to main content

React の `<A />` と `{a()}` の違い - Fiber ノードとフックの所属

TL;DR​

  • <A /> は React に「A 専用の Fiber ノードを作って」と伝える書き方。A の state, effect, key, ref はそのノードに紐づく
  • {a()} はただの関数呼び出し。戻り値(React Element)が親の children に展開されるだけで、A 専用のノードは作られない
  • a() の中で hooks を呼ぶと、それは 呼び出し元のフックとして登録される
  • React 公式ルール「Never call component functions directly」が禁じているのは「hooks を持つ関数を {a()} で呼ぶ」こと。hooks を持たない JSX 生成関数の呼び出しは対象外

このドキュメントの射程​

  • React で JSX に書く <A /> と {a()} の振る舞いの違いを Fiber tree レベルで整理
  • 公式ルール「Never call component functions directly」の適用範囲を明確化
  • render hooks パターンがなぜ「ライフサイクル変にならず」動くかを理解するための基礎
  • 「コンポーネントに切り出すか / ただの関数で十分か」の判断基準

内部メカニクス​

<A /> の場合​

<div><A /></div>
// ↓ Babel/TS が変換
React.createElement("div", null, React.createElement(A, null))

→ React は A を「コンポーネント」として認識し、Fiber tree に A 専用のノードを作る。state, effect, reconciliation, key, ref はこのノード単位で管理される。

{a()} の場合​

<div>{a()}</div>
// ↓
React.createElement("div", null, a())

→ a() は普通の関数呼び出し。戻り値(React Element)がそのまま親の children に展開される。a 専用の Fiber ノードは存在せず、a の中身は親のレンダー結果の一部として扱われる。

Fiber tree のイメージ:

<Child /> の場合:              {renderThing()} の場合:
Parent Parent
└── div └── div
└── Child ← ノード └── input ← 直接ここに展開
└── input (Parent の一部として扱われる)

公式ルール「Never call component functions directly」​

React 公式ドキュメント react.dev/reference/rules/react-calls-components-and-hooks には以下のルールがある:

Components should only be used in JSX. Don't call them as regular functions. React should call it.

そして bad example として <Layout>{Article()}</Layout> が挙げられている。

ただし**「なぜか」の理由**として示されているのは:

If a component contains Hooks, it's easy to violate the Rules of Hooks when components are called directly in a loop or conditionally.

つまりこのルールが厳密に問題視するのは hooks を持つ関数を {a()} 形式で呼ぶことであって、hooks を持たない純粋な JSX 生成関数は対象外と読める。

公式が挙げる、<A /> 形式が必要な理由:

  • Components become more than functions(hooks による local state がコンポーネントの identity に結びつく)
  • Component types participate in reconciliation(tree 構造の認識に使われる)
  • React can enhance your user experience(concurrent rendering 等の最適化のため)

hooks の所属がどう変わるか​

a() が hooks を呼ばない場合 → ただの JSX 切り出し​

// renderCheckbox は hooks を呼ばない。クロージャで checked を参照しているだけ
import { useState } from 'react';

export function App() {
const [checked, setChecked] = useState(false);

const renderCheckbox = () => (
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.currentTarget.checked)}
/>
);

// 下の 2 行は React から見て完全に同じ
return (
<div>
{renderCheckbox()}
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.currentTarget.checked)}
/>
</div>
);
}

何も問題ない。

a() が hooks を呼ぶ場合 → 親のフックとして登録​

import { useState } from 'react';

// ❌ アンチパターン: hooks を呼ぶ関数を {a()} で呼ぶ
function a() {
const [x, setX] = useState(0); // ← Parent のフックとして登録される
return (
<div>
<span>{x}</span>
<button onClick={() => setX(x + 1)}>+</button>
</div>
);
}

export function Parent() {
return <div>{a()}</div>;
}

つまり「ライフサイクルが変になる」のではなく、**「ライフサイクルの所有者が a から親 Parent に移る」**が正確な表現。

落とし穴 1: 条件付き呼び出しで Hooks ルール違反​

import { useState } from 'react';

function a() {
const [x, setX] = useState(0);
return <span>{x}</span>;
}

export function Parent({ condition }: { condition: boolean }) {
// ❌ a の中で useState を呼んでいるため、Hooks ルール違反
// condition が変わるとフックの呼び出し順が変わり、React が壊れる
return <div>{condition && a()}</div>;
}

落とし穴 2: state がシェアされる​

import { useState } from 'react';

function a() {
const [x, setX] = useState(0);
return (
<div>
<span>{x}</span>
<button onClick={() => setX(x + 1)}>+</button>
</div>
);
}

export function Parent() {
// ❌ a を 2 回呼ぶと、2 つの useState スロットが Parent に登録される
// 一見「2 つの独立した state」に見えるが、両方とも Parent のフック
// 呼び出し順に依存するため、a() の追加削除で全てが壊れる
return (
<>
<div>{a()}</div>
<div>{a()}</div>
</>
);
}

これを <A /> 形式で書くと正しく動く:

import { useState } from 'react';

function A() {
const [x, setX] = useState(0); // ← A 専用の Fiber ノードに紐づく
return (
<div>
<span>{x}</span>
<button onClick={() => setX(x + 1)}>+</button>
</div>
);
}

export function Parent() {
// ✅ それぞれ独立した state を持つ
return (
<>
<div><A /></div>
<div><A /></div>
</>
);
}

render hooks がなぜ動くか​

import { useState } from 'react';

export function useCheckbox() {
const [checked, setChecked] = useState(false);
// ↑ useCheckbox を呼んだコンポーネントのフックとして登録される

const renderCheckbox = () => (
// ↑ この関数自体は hooks を呼ばない。クロージャで checked を参照しているだけ
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.currentTarget.checked)}
/>
);

return [checked, renderCheckbox] as const;
}

export function App() {
const [checked, renderCheckbox] = useCheckbox();
return <div>{renderCheckbox()}</div>;
// ↑ ただの JSX 生成関数を呼んでいるだけ。Fiber ノードは増えない
}

React からの見え方:

  • App のフックとして useState(false) が登録される(useCheckbox 経由で)
  • App の render 結果として <div><input ... /></div> が出てくる
  • useCheckbox 用 / renderCheckbox 用の Fiber ノードは存在しない

→ 全部 App のライフサイクルに乗っかる。renderCheckbox 自体が hooks を呼ばない限り、Hooks ルール違反は起きない。

uhyo 氏の Qiita 記事でも、フックの中で別のフックを使う場合のフックの帰属について以下のように説明している:

ただし、実際のReactではステートはコンポーネントに属するものですから、コンポーネントをカスタムフックにした時点でuseStateはそのコンポーネントではなくuseCheckboxを呼び出した親側に属するように変わります。

振る舞いマトリクス​

hooks 使うhooks 使わない
<A /> で使う正しい使い方(コンポーネント)OK(state なしコンポーネント)
{a()} で使う危険(親のフックになる、条件付き呼び出しで Hooks ルール違反)完全に OK(ただの JSX 展開)

公式ルールが警告しているのは左下のセル。render hooks の renderCheckbox は右下のセル(hooks 使わない × {a()} 形式)に該当するので、Hooks ルール上の問題は起きない。

判断基準​

  • <A /> を使うべき → 独立したライフサイクル / 独立した state / key で識別したい / memo で再レンダー切り離したい場合。「ツリーにぶら下げて React に管理させたい」とき
// 各 ItemRow が独自の hover state や animation state を持ちたい
function ItemRow({ item }: { item: Item }) {
const [isHovered, setIsHovered] = useState(false);
return (
<li
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{ background: isHovered ? '#eee' : 'transparent' }}
>
{item.name}
</li>
);
}

function ItemList({ items }: { items: Item[] }) {
return (
<ul>
{items.map((item) => (
<ItemRow key={item.id} item={item} /> // ← それぞれ独立した isHovered
))}
</ul>
);
}
  • {a()} でいい → 単に JSX を切り出したいだけで、独立した管理対象にする必要がないとき。ただしその関数内で hooks を使ってはいけない
// 単に JSX の切り出し。hooks は使わない
function App() {
const [items, setItems] = useState<Item[]>([]);

const renderEmptyState = () => (
<div className="empty">
<p>アイテムがありません</p>
<button onClick={() => fetchItems().then(setItems)}>再読み込み</button>
</div>
);

return (
<div>
{items.length === 0 ? renderEmptyState() : <ItemList items={items} />}
</div>
);
}

まとめ​

  • <A /> は Fiber ノードを作る、{a()} は親に取り込まれる
  • hooks は呼び出し元コンポーネントに登録される
  • 公式ルール「Never call component functions directly」は厳密には「hooks を持つ関数を {a()} で呼ぶな」が本質
  • 「コンポーネントに切り出すかどうか」の判断基準は ライフサイクルを分けたいかどうか

参考​