ReactRouterでLazy読み込みをするついでにFragmentでルーティングファイルを分ける

PublicなルーティングとPrivateなルーティングが混在し、出し分けするパスも多い単一のReact Routerを、PublicとPrivateを分けるように修正して、Lazy読み込みを行うようにしてみました。
2020.02.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ども、もこ@札幌オフィスです。

今回はPublicなルーティングとPrivateなルーティングが混在し、出し分けするパスも多い単一のReact Routerを、PublicとPrivateを分けるように修正して、Lazy読み込みを行うようにしてみました。

現状

とあるプロジェクトでAmplify + AppSync + Cognito + Reactによるフロントエンド開発を行っており、aws-amplify-reactを重宝しながら認証、画面の出し分けを行っています。

index.tsx のuseEffectでログイン済みかどうかの判定を行い、Contextに挿入、React Routerによるルーティングを行っています。

const App: React.FC = () => {
  const state = useContext(LoginContext);
  useEffect(() => {
    async function getCurrentAuthenticatedUser() {
      const userInfo = await Auth.currentUserInfo();
      userInfo ? state.setLogin(true) : state.setLogin(false);
    }
    getCurrentAuthenticatedUser();
  }, []);

  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <LoginContextProvider>
        <Router>
          <Route exact path="/" component={Top} />
          <Route exact path="/posts" component={Posts} />
          <Route exact path="/post/:postID" component={Post} />
          ...多いので中略
          <Route exact path="/login" component={Login} />
          <PrivateRoute exact path="/dashboard" component={Dashboard} />
        </Router>
      </LoginContextProvider>
    </ThemeProvider>
  );
};

画面の出し分けをするPrivateRouterでは、Contextからログイン済みかを確認して、 <Route> のrenderで出し分けを行うようにしました。

const PrivateRoute = ({ component: Component, ...options }) => {
  const state = useContext(LoginContext);
  return (
    <Route
      {...options}
      render={props =>
        state.login ? <Component {...props} /> : <Redirect to="/login" />
      }
    />
  );
};

export default PrivateRoute;

このように一つのファイルにルーティング設定をまとめて書いていくような運用でも特に問題はありませんが、ルーティング設定が肥大化していくると可読性に欠けますので、ルーティングファイルを分けていきたいと思います。

Rotuerファイルの分割

今回はPublicなルーティングとPrivate(ログインが必須)なルーティングを分割したいので、下記のように分割したルーティングファイルを作成しました。

<> で挟むことにより、Fragmentを利用して、簡潔に複数のコンポーネントをまとめれるようにしています。

src/route/Public.tsx

const Public: React.FC = () => {
  return (
    <>
      <Route exact path="/" component={Top} />
      <Route exact path="/posts" component={Posts} />
      <Route exact path="/post/:postID" component={Post} />
      ...多いので中略
      <Route exact path="/login" component={Login} />
    </>
  );
};

export default Public;

src/route/Private.tsx

const Private: React.FC = () => {
  const state = useContext(LoginContext);
  useEffect(() => {
    async function getCurrentAuthenticatedUser() {
      const userInfo = await Auth.currentUserInfo();
      userInfo ? state.setLogin(true) : state.setLogin(false);
    }
    getCurrentAuthenticatedUser();
  }, [state]);

  return (
    <>
      <PrivateRoute exact path="/dashboard" component={Dashboard} />
    </>
  );
};

export default Private;

src/index.tsx

const App: React.FC = () => {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <LoginContextProvider>
        <Router>
          <Public /> {/* 子コンポーネントのルーティングを呼び出し */}
          <Private /> {/* 子コンポーネントのルーティングを呼び出し */}
        </Router>
      </LoginContextProvider>
    </ThemeProvider>
  );
};

上記により非常に簡単にルーティング設定を分割することができます。

ReactのFragmentを利用することにより、余計な <div> を挟む事なく出力することができますので、公式ドキュメントのサンプルにもある通りTableの作成等で重宝するかと思います。

Lazyを利用してコンポーネントをインポートする

コンポーネントの遅延読み込みをしていきましょう。

と言っても、やることは簡単で、React.lazyを利用して、importをちょっといじってあげて、 <Suspense><Route> を挟んであげるだけでOKです。

src/route/Public.tsx

import React, { lazy } from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";

const Top = lazy(() => import("../components/Top"));
const Posts = lazy(() => import("../components/Posts"));
const Post = lazy(() => import("../components/Post"));
const Login = lazy(() => import("../components/Login"));

const Public: React.FC = () => {
  return (
    <>
      <Route exact path="/" component={Top} />
      <Route exact path="/posts" component={Posts} />
      <Route exact path="/post/:postID" component={Post} />
      <Route exact path="/login" component={Login} />
    </>
  );
};

export default Public;

src/index.tsx

import React, { Suspense } from "react";
import { BrowserRouter as Router } from "react-router-dom";

import PublicRouter from "./route/Public";
import Private from "./route/Private";
import Header from "./components/Header";

const App: React.FC = () => {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <LoginContextProvider>
        <Router>
          <Suspense fallback={<Header />}> {/* 読み込み中に出す画面を指定 */}
            <Public />
            <Private />
          </Suspense>
        </Router>
      </LoginContextProvider>
    </ThemeProvider>
  );
};

<Suspense> にはfallbackで読み込み中の表示を指定する必要があります。

こちらのfallback内にてコンポーネントを利用することができますが、利用するコンポーネントはLazyで読みこむとエラーになるので注意が必要です。

まとめ

・React RouterのルーティングにはFragmentを利用する!

lazy() でサクッとローディング画面を実装できて超便利!

誰かのお役に立てれば幸いです。