背景
React Routerを使ったアプリで、ルーティング定義をファイル分割しようとした。HomeRoutesという関数コンポーネントを作り、<Routes>の中で<HomeRoutes />として呼び出したところ、以下のエラーが発生した
Error: [HomeRoutes] is not a <Route> component.
All component children of <Routes> must be a <Route> or <React.Fragment>
関数コンポーネントの中で<Route>を返しているのだから結果的に同じでは?と思ったが、そうではなかった。この記事では、なぜこのエラーが発生するのか、その原因と解決策、そして<Routes>の内部設計まで掘り下げて記録
ソースコード
エラーが出たコード
// HomeRoutes.tsx(関数コンポーネントとして定義)
export const HomeRoutes = () => {
return (
<Route path='/home' element={<Home />}>
<Route path='setting' element={<Setting />} />
<Route path='user_management' element={<UserManagement />} />
</Route>
)
}
// Router.tsx
export const Router = memo(() => {
return (
<Routes>
<Route path="/" element={<Login />} />
<HomeRoutes /> {/* ← ここでエラー */}
</Routes>
)
})
修正後のコード
// HomeRoutes.tsx(変数としてJSX要素を直接代入)
export const HomeRoutes = (
<Route path='/home' element={<Home />}>
<Route index element={<HomeContent />} />
<Route path='setting' element={<Setting />} />
<Route path='user_management' element={<UserManagement />} />
</Route>
)
// Router.tsx
export const Router = memo(() => {
return (
<Routes>
<Route path="/" element={<Login />} />
{HomeRoutes} {/* ← {} で展開する */}
</Routes>
)
})
詰まったところ
HomeRoutesを関数コンポーネントとして定義し、<HomeRoutes />で呼び出していた。内部で<Route>を返しているのだから問題ないと思っていたが、<Routes>は受け付けてくれなかった
直感的には「return で <Route> を返してるんやから同じ」と思うが、<Routes>はそういう判定をしていない
原因
React Router v6の<Routes>は、子要素をレンダリングせずにpropsをデータとして直接読み取る設計になっている。3ステップに分けて説明する
Step1. JSXは「オブジェクト」を作るだけ
JSXを書いた時点では、まだ何も実行されていない。ただのJavaScriptオブジェクトが作られるだけ
<Route path="/home" element={<Home />} />
これは内部的にはこういうオブジェクトになる:
{
type: Route,
props: { path: "/home", element: <Home /> }
}
JSXを書く = オブジェクトを作る。これだけ。この時点ではRoute関数もまだ呼ばれていない
Step2. 「レンダリングする」とはどういうことか
通常のReactコンポーネント、たとえば<div>の場合:
<div>
<MyComponent name="太郎" />
</div>
Reactは<div>の子を処理する時に、MyComponent関数を実際に呼び出す(=レンダリングする)
// Reactが内部でやってること(こんなイメージか?)
const result = MyComponent({ name: "太郎" }) // ← 関数を実行する
この「関数を実行して、返ってきたJSXをさらに処理する」のがレンダリング
Step3. <Routes>は「レンダリングしない」
<Routes>は子要素に対して、上のような関数呼び出しをしない。代わりに、Step1で作られたオブジェクトのtypeとpropsを直接読むだけ
<Routes>
<Route path="/" element={<Login />} />
<Route path="/home" element={<Home />} />
</Routes>
// Routes が内部でやってること(こんな感じか?)
function Routes({ children }) {
const routes = []
React.Children.forEach(children, child => {
// child はオブジェクト: { type: Route, props: { path: "/", element: ... } }
// オブジェクトの中身を直接読むだけ。Route関数は一切呼ばない
routes.push({
path: child.props.path, // "/" とか "/home"
element: child.props.element, // <Login /> とか <Home />
})
})
// 集めたルート情報から、現在のURLに合うものを1つ選んでレンダリング
const match = findBestMatch(currentURL, routes)
return match.element
}
Route関数は一切呼ばれていない。オブジェクトのpropsを読んでるだけ
だから関数コンポーネントはダメ
ここまでの流れで、なぜ<HomeRoutes />がダメかがわかる
<Routes>
<HomeRoutes />
</Routes>
この時、<Routes>が受け取る子オブジェクトはこうなる:
{
type: HomeRoutes, // ← Route じゃない
props: {} // ← path も element もない
}
<Routes>はこのオブジェクトのtypeを見て「Routeじゃない」と判定する
なぜこの設計なのか
URLマッチングは全ルートを一括で比較して最適なものを選ぶ必要がある。もし子を一つずつレンダリングしてから判定する設計だと:
- 全コンポーネントを実行しないとどのルートが一致するかわからない
- 副作用のあるコンポーネントが意図せず実行される可能性がある
これを避けるため、<Routes>は「レンダリングせずに設定だけ読む」という設計を採用しているらしい。その制約として「<Route>以外は受け付けない」というルールが存在する
関数コンポーネントと変数の違い
今回の修正の本質は、HomeRoutesの中身を「関数」から「値」に変えたこと
// 関数が入っている → 呼び出さないと中身が出てこない
const HomeRoutes = () => (
<Route path='/home' element={<Home />} />
)
// 値そのものが入っている → そのまま使える
const HomeRoutes = (
<Route path='/home' element={<Home />} />
)
シンプルな例で考えると:
const x = () => 5
x // → 関数そのもの
x() // → 5
const y = 5
y // → 5
解決策
HomeRoutesを関数コンポーネントではなく、<Route>要素そのものを持つ変数に変更。呼び出し側も<HomeRoutes />ではなく{HomeRoutes}で展開するように修正
これにより<Routes>の中に<Route>が直接置かれるのと同じ状態になり、エラーが解消された
おまけ:indexルートでデフォルト表示を設定する
修正時に<Route index>も追加した。これは親ルートのパスにぴったり一致した時に表示される子ルート
<Route path='/home' element={<Home />}>
<Route index element={<HomeContent />} />
<Route path='setting' element={<Setting />} />
<Route path='user_management' element={<UserManagement />} />
</Route>
| URL |
<Outlet />に表示される内容 |
|---|---|
/home |
<HomeContent /> |
/home/setting |
<Setting /> |
/home/user_management |
<UserManagement /> |
indexがないと、/homeにアクセスした時に<Home />内の<Outlet />部分が空になる。indexルートはその空を埋めるデフォルトコンテンツという役割
感想
- 関数と値の違い、JSXの世界でも普通にJavaScriptの基本が効いてくるというのが改めて実感できた
-
<Routes>の内部実装まで追うと、なぜその制約があるのかが腑に落ちる。エラーメッセージの裏にある設計思想を理解するのは大事
参考