😸

React Router v6.4はClient Side Remix

2022/09/21に公開

React Router v6.4 が、破壊的変更を伴わないマイナーバージョンアップながら、機能追加の内容としてはかなり新しい世界観を追加してきたので、5 分くらい眺めて気づいたことをメモしておきます。

Routing と Data Loading/Mutation の統合

まずはリリースノートがこちら。

https://remix.run/blog/react-router-v6.4

そしてリリースノート内に貼り付けられている動画がこちら。

https://www.youtube.com/watch?v=aX74DbFT5nI

1:45 しかない動画なので、動画を見ていただければこの記事はあんまり読まなくてもいいかもです。

さて、動画内では、次の 2 つの機能を <Route> コンポーネントへと統合した話題が語られています。

  • データ読み込み(Data Loading) 0:00〜
    • 0:28〜 エラーハンドリングの話題もあり
  • データ更新(Data Mutation) 0:52〜

<Route> に統合された API は、ざっくりと次のような形です。

<Route
  path="/hoge/:hoge_id"
  element={<HogePage />}
  // データ読み込み
  loader={async ({ params }) => {
    const hogeData = await apiClient.getHogeData(params.hoge_id); // pathのパラメータを取り出せる
    return hogeData;
  }}
  // エラーハンドリング
  errorElement={<HogeLoadingErrorPage />}
  // データ更新
  action={async ({ params, request }) => {
    const formData = request.formData(); // Web標準のFetch APIのRequestオブジェクトを扱える
    return apiClient.updateHogeData(formData, params.hoge_id);
  }}
>

それぞれどんな時に呼ばれるのか簡単に見ていきます。

Data Loading

Route コンポーネントの loader 属性に登録した関数は、基本的に画面の表示前に呼び出されます(おそらく後述する action の後にも呼び出されます)。同じチームが作っている Remix にも同名の APIがあって、あちらはサーバーサイドレンダリングのためにサーバー側でデータ読み込みを行うのですが、React Router は素直に使った場合はブラウザ上でしか動かないライブラリなので、いま言及している loader はブラウザ上でのデータ読み込みをお手伝いする機能となります。

読み込んだデータは、React Router v6.4 で追加された新機能である useLoaderData フックで結果を受け取ることができます。

const HogePage = () => {
  const hogeData = useLoaderData(); // loaderでreturnしたものが出てくる

  return <>...</>;
};

loader 属性を定義した場合、 <Route> はデータ読み込みが成功した場合だけ element 属性に登録された <HogePage> をレンダリングするので、 useLoaderData() の戻り値を受け取る時点では、データ読み込みが成功していると見なすことができ、安心感があります。

データの読み込みが失敗した場合は、 errorElement に登録してあるコンポーネントへと表示がフォールバックされます。

errorElement={<HogeLoadingErrorPage />}

Data Mutation

Route コンポーネントの action 属性に登録した関数は、表示中のコンポーネント内で、新設の <Form> コンポーネントによる submit 処理が行われた時に呼び出されます。

使い方としては、ほとんど HTML の <form> と変わりません。

<Form method="put">
  <input type="text" name="name" />
  <input type="text" name="description" />
  <button type="submit">Update</button>
</Form>

ここで submit 処理を行うと、action に登録された関数へと formData が渡され、通信処理が非同期で行われます。

なぜ Routing と Data Loading/Mutation を統合するのか

というわけで、Routing と統合されたデータの読み書きについて見てきました。

正直、「これはもう色々くっつきすぎて、Router と呼べるライブラリではないのでは……?」という感想を持った方もいると思います。わかる。とはいえ、これがあくまでもマイナーバージョンアップであることからもわかるように、従来の使い方を全く阻害するものではないので、データの読み書きを伴わない運用をしても全然問題ありません。

なんでわざわざ統合するのかというと、既に React 世界のパラダイムとして、データ読み込みとルーティングの距離が近くなっているから、という理由が考えられます。

例えば、シングルページアプリケーションの時代から、認証処理とルーティングは密に連携するべき機能でしたよね。あれも広義では通信とルーティングを統合して扱うことが求められているユースケースでした。

また、Next.js が流行ったことによって、 getServerSideProps のように、コンポーネントのすぐ隣にデータ読み込みの関数を置いて初期表示のデータ読み込みをそちらへ任せる、つまり、コンポーネント内では初回データ読み込みの非同期処理のハンドリングを行わない実装というものは、React コミュニティで受け入れられやすいものになりました。

React Router の開発元である Remix チームも、コンポーネントのすぐ近くにデータ読み込み関数を置くパラダイムを Remix で先行実装しており、それが React Router に逆輸入された形になります。

前述したサンプルでは、コンポーネントというよりは <Route> の近くにデータ読み込み関数を定義していましたが、次のように工夫すると、Remix ライクな(そして Next.js ライクな)形で初回データ読み込みを実装できます。

HogePage.js
export const loader = async ({ params }) => {
  const hogeData = await apiClient.getHogeData(params.hoge_id);
  return hogeData;
}

const HogePage = () => {
  const hogeData = useLoaderData();

  return <>...</>;
};

export default HogePage;
routes.js
import * as HogePage from "./HogePage";

<Route
  path="/hoge/:hoge_id"
  element={<HogePage />}
  // データ読み込み
  loader={HogePage.loader}
  // エラーハンドリング
  errorElement={<HogeLoadingErrorPage />}
>

HogePage.js の見た目が、だいぶ Remix っぽくなりました。

Remix ではサーバーで実行される loader 処理が、React Router ではブラウザ側で実行されるのが違いです。

クライアントサイドで Remix ライクな API が扱える

「API の形がほとんど同じなら、Remix でいいじゃん。なんでわざわざ React Router に組み込んだの」という疑問は、私も真っ先に思いました。

Next.js の exportみたいなことをやりたかったのかな、まで想像したところで、ふと思い出しました。Next.js は export によって SPA 化するとgetServerSideProps が動かないということを。

React Router はもちろん SPA として動かすのが主な用途ですが、Remix とほぼ同じパラダイムで loaderaction を設置できています。この違いはなんでしょうか。

そもそも Next.js の getServerSideProps が SPA 時に動かせないのは、これが Node.js のランタイムで動かすことを前提とした関数だからです。ブラウザで動かすことができないのは、かなり自然なことです。

では、ほぼ同等の機能として見られることが多い、Remix の loader はどうでしょうか。実は、Remix には可能な限り Web 標準に則った API を整備するというポリシーがあり、Remix の loader 内でもグローバルスコープから直接呼び出せる API 群はブラウザから使えるものに寄せた形になっています。もちろん、実用上は Node.js や Deno 等のランタイムに依存した処理が必要になることもありますが、そういった場合は @remix-run/node@remix-run/deno のようなパッケージの機能を経由することが推奨されています。元々、Remix の loader 内は、ブラウザに近い感覚で fetch 等の関数をそのまま扱えるスコープになっているのです。

そういった事情もあり、Remix の loader には特定のサーバーサイド JS ランタイムに依存しておらず、むしろブラウザのインターフェースに依存している特性があったため、React Router で SPA 向けに同等の API を提供しても、混乱が起こりづらいと判断されたのでしょう。

これだけだと、Remix で静的エクスポートをしても loader が動く、という話ではないので、Next.js のそれと比べるのは現状ではちょっとずるいのですが、この分ならそういう機能が実装される日も近いのではないでしょうか。知らんけど。

実用上の話でいうと、React Router で実装してある既存のアプリケーションを Remix に移植したくなった場合に、まずは React Router のまま loaderaction 等を実装して、Remix っぽい構成になるようリファクタリングを進めておく、という段階を踏むことができるようになります。「Remix Ready な SPA プロジェクト」という段階に到達した時点で、Remix への移植を行えば、これは大きな手間にはならないはずです。

まとめ

初見ではなんとなく Remix を逆輸入しただけかなーと思ったのですが、段階的移行の話や、静的エクスポートしても動く loader が生まれるかも?という話など、意外と面白い方向に話を膨らませることができました。

ここから先は君たちの目で確かめてくれ!

宣伝

株式会社モニクルでは、はたらく世代・子育て世代がお金の不安を手放せる手助けをするための金融サービス業をより広めるために、ソフトウェアエンジニアを募集しています。

90 秒だけお時間をいただいて、↓ の動画を見ていただけると、どんなサービスをやっている会社なのかざっくりわかってもらえると思います。

https://www.youtube.com/watch?v=mwZ_tnGKEIY

マネイロにお金の相談をしにきてくださるだけでも嬉しいですし、もしこの記事を読んで会社自体にも興味を持っていただけたら、↓ の Culture Deck(会社説明資料)を読んでみてください。

https://speakerdeck.com/monicle/about

さらに興味を持ってもらえた方は Meety で私と雑談しましょう!

https://meety.net/matches/egOQbaoKmgHd

よろしくお願いします!

株式会社モニクル

Discussion