大きく機能が変わる?React Router v6.4 Pre-releaseをためしてみた

React Routerがv6.4で再び大きく変化するようです。まるでReactがCSR版のRemixになる?まず素振りがてらPre−release版をためしてみました。
2022.05.31

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

はじめに

こんにちは、CX事業本部MAD事業部の森茂です。
React Routerがv6.4で再び大きく変化するようです。3月末に下記Remix公式ブログで事前の案内はありましたがReact Routerと同じチームが開発しているRemixの思想がReact Routerに取り込まれ、そのPre-release版とData APIについてのドキュメントが先日先行して公開されました。

Release Remixing React Router · remix-run/react-router

React Routerの安定版は2022年6月現在 v6.3.0となります。

React Router v6.4 Pre-release

React Routerがバージョンアップと聞くと身構える方も多いかもしれません:)

今回は機能(主にData API関連)の追加ということのようで、従来のReact Routerに(v6自体がすでに大きく変わっていますが...)、Loader FunctionAction FunctionFormコンポーネントといったRemixならではの抽象化された機能が多数追加されています。6-3->6.4という進化からは想像できない範囲の機能が追加されルーティングライブラリというよりはRemixのCSR版フレームワークと言えるくらいの変革になるかもしれません。

ためしてみた

公式のデモもありますが、いったん手元でもCRAを利用して動作させてみました。

$ yarn create react-app react-router-pre --template typescript
$ cd react-router-pre
$ yarn add react-router-dom@next

ルーティング用のコンポーネント

v6.4で追加されたData Loaderを利用するにはRoutesではなくDataBrowserRouterの中にRouteを展開するようです。Remixの特徴でもある並列のデータフェッチがv6.4でも利用できるようになったので、親子関係のコンポーネントともに用意してみます。

React RouterではRemixのLoader Functionを再現するために、ページコンポーネントとともにLoaderもインポートし、自身のコンポーネントへRouteを経由して渡すようです。

src/App.tsx

import './App.css';
import { DataBrowserRouter, Route } from 'react-router-dom';
import { Root, loader } from './routes/root';
import { TodoList, loader as todoListLoader } from './routes/todoList';

function App() {
  return (
    // fallbackElementでデータフェッチ完了までの表示をフォールバックする
    <DataBrowserRouter fallbackElement={<div>Loading...</div>}>
      // 自身からexportしているloaderをコンポーネントに渡す
      <Route path="/" element={<Root />} loader={loader}>
        <Route
          path="/todoList"
          element={<TodoList />}
          loader={todoListLoader}
        />
      </Route>
    </DataBrowserRouter>
  );
}

export default App;

ルートページ(兼レイアウト)

/で表示されるルートページを用意します。Outletを利用して/todoListroot.tsxをレイアウトとしても利用し親子関係を用意します。Remixを利用されている方にはおなじみのjsonヘルパーもあるようです。いったんエクスポートしたloaderRouteを経由してコンポーネント側のuseLoaderDataで受け取っています。

src/routes/root.tsx

import { json, LoaderFunction, Outlet, useLoaderData } from 'react-router';
import { Link } from 'react-router-dom';

type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

export const loader: LoaderFunction = async () => {
  const response = await fetch(
    'https://jsonplaceholder.typicode.com/todos?_limit=5',
  ).then(async (response) => {
    return response.json();
  });

  return json<Todo>(response);
};

export const Root = () => {
  // Routeを経由してloaderの中身を受け取る
  const data = useLoaderData() as Todo[];

  return (
    <div>
      <h1>Root Component</h1>
      <div>
        <Link to="todoList">Link</Link>
      </div>
      <ul>
        {data.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <hr />
      <Outlet />
    </div>
  );
};

子ページ(/todoList

root.tsxをレイアウトとして持つtodoListページを用意します。

src/routes/todoList.tsx

import { json, LoaderFunction, useLoaderData } from 'react-router';

type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

export const loader: LoaderFunction = async () => {
  const response = await fetch(
    'https://jsonplaceholder.typicode.com/todos?_limit=3',
  ).then(async (response) => {
    return response.json();
  });

  return json<Todo>(response);
};

export const TodoList = () => {
  const data = useLoaderData() as Todo[];

  return (
    <div>
      <h1>todoList</h1>
      <ul>
        {data.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
};

ここまで用意したところで開発サーバーを起動します。

$ yarn start

これで、http:localhost:3000/root.tsxが、http://localhost:3000/todoListroot.tsxをレイアウトとして利用しているtodoList.tsxが表示されます。

開発者ツールで通信速度をFast 3GにしてためしてみるとDataBrowserRouterfallbackElementで用意したloading...の文字が確認できると思います。内部的にはSuspenseを利用して実現しているようです。

またデータ取得に関しても/でアクセスしてLinkボタンから/todoListへ遷移した場合はそれぞれ1回のデータ取得を行い、直接/todoListを開いた場合は並列にデータ取得を行いページの表示速度を早めているのがわかります。

レイアウトルート

個人的に注目している活用方法としては、Routepathを指定しない形でpathを持ったRouteを囲むことでレイアウトのみの用途として利用する使い方です。RemixだとPassless layout routeという機能で実現したりします。ちょうど先日発表されたNext.jsのRFCでもlayout.jsとして話題にあがっていたものと似たようなものです。

layout-route

pathなしのRouteコンポーネントでもloaderactionが利用できるのでRouteの使い方の幅はかなり広くなりますが、それなりに複雑にもなりそうです:)

src/App.tsx

function App() {
  return (
    // pathを渡さずにRouteを囲むことでレイアウトのみの用途としても動作する
    <DataBrowserRouter fallbackElement={<div>Loading...</div>}>
      <Route element={<Root />} loader={loader}>
        <Route path="/" element={<TodoList />} loader={todoListLoader} />
      </Route>
    </DataBrowserRouter>
  );
}

もちろんこの形でもそれぞれのloaderのデータ取得は並列に行われます。

ところで

ルーティングライブラリでデータ取得も、ということであれば、React Routerとは別にReact Queryの作者でもあるTanner Linsley氏が開発しているReact Locationというルーティングライブラリも昨年11月にリリースされています。

こちらも遷移時にデータの取得を並列で行い、ドキュメント上ではParalizeという言葉を利用されていますが、データの完了を待ってコンポーネントを表示するという手法をとっているようです。React Routerとの大きな違いとしてはこのデータ取得した値をキャッシュとして保持できる点になるでしょう。React Query同様に開発者ツールがあるのでキャッシュの値を確認しながら開発できる点もメリットとなりそうです。

さいごに

とても簡単にではありますがReact Router v6.4 Pre-release版をためしてみました。Remixを利用しているユーザーからするとLoaderだけでなくActionFormuseFetcherなどよく利用するものがそのまま同じような使い方で利用できるので、違和感なく導入できるかもしれません。

v6.4の公式ドキュメント(絶賛拡張中)公式のデモでも他にどのような追加があるかぜひ確認してみてください。

ただ変化は大きく、もうすでひとつのルーティングライブラリではなくRemixのCSR版フレームワークという位置づけの方が正しいかもしれません。これはこれで新たに物議を醸すバージョンアップとなりそうです。