2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Router `<Routes>`の子にルート分割した関数コンポーネントを置いたらエラーになった話

2
Posted at

背景

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で作られたオブジェクトのtypeprops直接読むだけ

<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>の内部実装まで追うと、なぜその制約があるのかが腑に落ちる。エラーメッセージの裏にある設計思想を理解するのは大事

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?