🎉

react-router-dom V5のNested Routeに関して

2022/06/27に公開

はじめに

初めまして!
株式会社おてつたびでフルスタックエンジニアをしているぶりぼんと申します。主にフロントエンド領域を開拓しており、ReactやTypeScriptが最も得意です。

おてつたびでは、フロントエンドのライブラリにReactを、ルーティングにはreact-router-domを使用しています。
今回は、react-router-domのバージョン5でNested Routeを行う際に、はまったポイントを解説していきます。

本題

実装したかったこと

おてつたびでは、Webプロダクト開発と一緒に、iOSアプリ開発も行っております。
iOSアプリを開発する中で、時間やコストの関係上、WebView対応せざるを得ないページが出てきます。
WebView対応するページは、独自のプレフィックス(本記事ではサンプルとして/native_appとしています)をつけることで、WebView時に表示したくないものを条件分岐で非表示にするという対応をとっております。

以上を踏まえて、通常のWebページ表示とWebView表示を行うために、react-router-domで以下のように設定をしました。
(実際に書いたコードとは大きく異なるのと、コードの多くを省いて記載しています。)

router.tsx
const Router = () => {
  return (
    <BrowserRouter>
      <Switch>
        <SharedRoute />
        <Route path="/native_app">
          <SharedRoute />
        </Route>
        <Route path="/other_route" component={OtherRouteComponent} />
      </Switch>
    </BrowserRouter>
  )
}
SharedRoute
const SharedRoute = () => {
  const concatNativeApp = (path: string) => {
    const isNativeApp = window.location.pathname.includes("/native_app")
    return isNativeApp ? `/native_app${path}` : path
  }
  return (
    <>
      <Route path={concatNativeApp("/hoge/foo")} component={HogeComponent} />
      <Route path={concatNativeApp("/hoge/bar")} component={FooComponent} />
    </>
  )
}

上記のコードでは、/hoge/foo/hoge/barの両ページは表示されますが、/native_app/hoge/foo, /native_app/hoge/bar, /other_routeの3つのページは表示されません。
react-router-domのSwitchコンポーネントのコードを見ると、なぜこれが動作しないのかがよく分かります。
まずは、どうすればこのコードが動くかを提示します。

動作するコード

結論を述べると、SharedRouteコンポーネントを、Routeコンポーネントでラップする必要があります。
これで、定義している5つのページが表示されます。

router.tsx
const Router = () => {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/hoge">
          <SharedRoute />
        </Route>
        <Route path="/native_app">
          <SharedRoute />
        </Route>
        <Route path="/other_route" component={OtherRouteComponent} />
      </Switch>
    </BrowserRouter>
  )
}
SharedRoute
const SharedRoute = () => {
  const concatNativeApp = (path: string) => {
    const isNativeApp = window.location.pathname.includes("/native_app")
    return isNativeApp ? `/native_app${path}` : path
  }
  return (
    <>
      <Route path={concatNativeApp("/hoge/foo")} component={HogeComponent} />
      <Route path={concatNativeApp("/hoge/bar")} component={FooComponent} />
    </>
  )
}

なぜ動作しないのか

以下のreact-router-domのコードが、なぜ最初のコードでは動作しないかの理由となります。
https://github.com/remix-run/react-router/blob/3679bafb744c1fb1b495eefdacce0a4fbbb9197a/packages/react-router/modules/Switch.js

原因は、Switchコンポーネントにあります。
SwitchコンポーネントのURLパスのパターンマッチングは、propsに格納されているchildrenをforEachで行っています。
さらにその内部で、子コンポーネントのpathの取得はconst path = child.props.path || child.props.from;で行っており、再帰的にpathの取得は行っておりません。

SharedRouteコンポーネントでRouteコンポーネントを使ってルーティング情報を返しているので、一見すると動作するように見えます。(現に弊社エンジニアは私を含め、このコードで動作すると思っていました。。)
しかし、SharedRouteコンポーネント自体がpathfromでURLパスの情報を持たなければ、Switchコンポーネントによるルーティングが機能しません。

ここからは推測も含みますが、PasswordResetRouteコンポーネントがルーティング用のコンポーネントではなく、通常のコンポーネントとして処理されます。
一方で、PasswordResetRoute内でもルーティング情報を有しているため、通常のコンポーネントとして処理されたこのコンポーネント内のページが先にマッチングすることで、他のページが表示されないという結果になっているのではないかと思われます。

react-router-domは扱いに癖があり、公式ドキュメントやコードを読まないと正しく使えないことが多々あります。
直感に反する動作をしている一方で、コードを読むことで解決するので、ルーティングが正しく動作しない場合、落ち着いてコードを読んでみることをお勧めします。
また、react-router-domは最新のバージョンで6系がリリースされています。
軽くドキュメントを読む限りでは、直感的でシンプルに書けるようになっているようです。
フロントエンドエンジニアとしては、弊社でも早く取り入れてみたいとうずうずしています。

終わりに

株式会社おてつたびでは、一緒に働く仲間を募集しています!
本記事では、今まで行ってきたWeb開発における話をしましたが、iOSアプリの開発を行っており、初期リリースを控えております。
第一号となるiOSエンジニアを募集しておりますので、まずは興味がある方は気軽にお話をしましょう!
もちろん、Webフロントエンドエンジニア、Webバックエンドエンジニアも募集しております。

引き続き、地域とのご縁を紡げるサービスであり続けるために、多くの方に興味を持ってもらいたいです。
本記事をご覧になって少しでも興味が湧いた方は、以下のリンクをチェックしてみてください!

おてつたび 採用情報
Wantedly 採用ページ
Wantedly iOSエンジニア募集
ぶりぼんのMeety

Discussion