Streaming APIに対応したRemix 1.11のリリースノートを流し読む
Remix の v1.11 が出ました。適当に感想を書きます。
クライアント側で完了を待つ defer サポート
React 18 には Streaming という機能があるんですってね。
「ファーストビューの表示には間に合わなくてもいいから、ちょっと遅れてでも表示したい」みたいなデータを、サーバー側で用意しつつ、クライアント側では <Suspense>
で待っててもらい、データの用意が出来次第サーバーからクライアントに流し込んで表示、みたいなユースケースで役立つ技術だと思っています。しらんけど。
何ができるようになるかというと、 loader()
内で「別にこれ初回表示に間に合わなくてもいいな」というデータを、次のように Promise が完了しないまま return できます。その際、いつもの json()
ではなく defer()
で囲む必要があります( JSON.stringify(promise)
になっちゃうのも気持ち悪いので納得感はある)。
import { defer } from "@remix-run/node";
export async function loader({ request }) {
// こっちはawaitしているので、取得済みの実データが変数に入る
let productInfo = await getProductInfo(request);
// こっちはPromiseの完了を待たずに、Promiseオブジェクトのまま変数に入れている
let productReviewsPromise = getProductReviews(request);
// Promise状態のデータをクライアントに流し込むためのおまじない
return defer({
productInfo, // loader()の実行完了までにデータが出揃うので、ファーストビューのHTMLに埋め込まれるし、useLoaderData()からは実データが出てくる
productReviewsPromise, // この時点でデータが取得できていないので、HTMLには埋め込まれないし、useLoaderData()からもPromiseオブジェクトが出てくる
});
}
で、 defer
経由でコンポーネントに渡された Promise は、 useLoaderData()
で取り出すと、やはり Promise のままになっています。この Promise の結果を <Suspense>
で待って UI に表示するのが次の例です。
import { Await } from "@remix-run/react";
function ProductRoute() {
let {
// loader()時点でデータ取得が終わっているので、この時点で必ずデータが存在する
productInfo,
// Promiseのまま届くので、初期表示のこの時点ではまだデータがないかもしれない
productReviewsPromise,
} = useLoaderData();
return (
<div>
<h1>{productInfo.name}</h1>
<p>{productInfo.description}</p>
<BuyNowButton productId={productInfo.id} />
<hr />
<h2>Reviews</h2>
{/* productReviewsPromiseの完了を待つためのSuspense */}
<React.Suspense fallback={<p>Loading reviews...</p>}>
{/* RemixのAwaitコンポーネントを使うと、データ取得の完了を待ってからUIを表示できる */}
<Await resolve={productReviewsPromise} errorElement={<ReviewsError />}>
{(productReviews) =>
productReviews.map((review) => (
<div key={review.id}>
<h3>{review.title}</h3>
<p>{review.body}</p>
</div>
))
}
</Await>
</React.Suspense>
</div>
);
}
// Promiseのエラーハンドリング
function ReviewsError() {
let error = useAsyncError(); // エラー内容を取り出す
return <p>There was an error loading reviews: {error.message}</p>;
}
この場面で、Suspense をハンドリングする役割を担うのが、Remix の <Await>
コンポーネントです。Promise の完了を待って、成功すれば children に結果データを渡して表示しますし、失敗すれば errorElement
属性に設定したコンポーネントでエラー表示を行います。
まあまあ直感的に見えるので、忘れそうになりますが、これは「 getProductReviews(request)
が発行したデータ取得処理の Promise がブラウザに渡されてきて Suspense で完了を待てる機能」ではないはずです。サーバー側で生成した Promise は内部でサーバー側のマシンリソースを抱えている非同期処理の管理オブジェクトなので、そもそもシリアライズして他所に送ることもできません。
ざっくりこんな感じの役割分担をしてそうだなと思いました。
- サーバー側:
getProductReviews(request)
が発行した Promise の完了を待って、クライアントに結果を通知する(defer
の責務) - ブラウザ側:
getProductReviews(request)
のことは全く知らないが、後でサーバーから Streaming API 経由で何らかのデータが送り込まれてくるらしいので待ってる
ブラウザ側はあくまでも、 loader()
と似たような形で後追いで送られてくるデータを Promise で待っているだけで、 getProductReviews(request)
由来の Promise を持っているわけではない、という感じですかね。
まあソースコードや公式ドキュメント読んだら詳しいところもわかると思うのですが、僕が読むのは面倒なので気になった方は読んでみてください(僕が間違ったことを言っている可能性もあります)。
- https://remix.run/docs/en/v1/guides/streaming
- https://remix.run/docs/en/v1/components/await
- https://remix.run/docs/en/v1/utils/defer
まあでも、直感的には「 getProductReviews(request)
が発行したデータ取得処理の Promise がブラウザに渡されてきて Suspense で完了を待てる機能」に見えるので、書き味は良さそうですよね。上記のことを意識せずに乱暴なコードを書くと破綻しそうですが、意識して書く分には便利に使えそう。
React Router からの移植機能
ちなみにこの機能、React Router で先行実装されていた defer
や <Await>
の Remix 移植版だったりします。
React Router では処理系を跨がない(ブラウザ内で全て完結する)ため、実装しやすかったのでしょうね。React Router をインターフェースの叩き台として利用して、ある程度固まったところで、処理系をまたぐ(Node.js の処理完了をブラウザで待つ)Remix 版の実装に入ったのだと思います。
面白い機能だったので、待ってました!という感じです。
CSS のバンドルをサポート(unstable)
これまだよくわかってないんですけど、 import { cssBundleHref } from "@remix-run/css-bundle";
の cssBundleHref
を links
に登録しておくといい感じになるらしい。
でもサンプルを見ても cssBundleHref
を links
に登録してないみたいで「よくわからんな?」という顔になっています。
Remix は UI ライブラリ対応がちょっと雑め(tailwind 以外は大なり小なり問題が起きがち)なので、こっち方向の改善はどんどん進めていってほしいです。
Remix v2 の route 定義(Flat Routes)の先行公開
v2 からは、新しい形式の route 定義方法である、Flat Routes が使えるようになります。あくまでも追加仕様なので、従来のディレクトリを掘り下げるやり方も継続して使えます。
細かいところは ↓ の RFC を見てほしいです。
ざっくりと言うと、
routes/auth/github/callback.tsx
みたいに書いていた route を、
routes/auth/github.callback.tsx
のように、ドット区切りで書いても、どちらも /auth/github/callback
という URL を表現する定義になります。
つまり、
-
routes/
-
auth/
-
github/
callback.tsx
-
twitter/
callback.tsx
-
google/
callback.tsx
-
-
のように書いていた階層を、
-
routes/
-
auth/
github.callback.tsx
twitter.callback.tsx
google.callback.tsx
-
と書き換えることができます。たぶん(実験してない)。
URL の設計にソースコードの階層が引きずられることで、見通しが悪くなることがありましたが、これでスッキリしますね。
サンプルでたまに見かけてた
ちょっと前にどこかのサンプルコードで上記のようなファイル名を見かけていたのですが、Flat Routes だったんですねえ。手元で同じファイル名を試しても動かなかったので、不思議に思っていました。
まとめ
defer
も Flat Routes も、開発体験をまあまあ良くしてくれそうなので、楽しみですね。
Discussion
CSS のバンドリングについては、これまで生の(あるいはユーザが自力で生成した) CSS を
import
してlinks
に渡すしかなかったのが、各種のバンドリング手法に対応してそのバンドル結果がcssBundleHref
に渡されるということのようです。それを生のCSS同様にlinks
に渡すようリリースノートには書いてありますが、個々のバンドリング技術については書いてなく確かに docs を読まないと掴みづらかったですね。これまで route ごとのスタイルを書くだけなら足りていましたが、 CSS と紐付いたコンポーネントなどを扱いやすくなりそうで、私も今後の改善に期待しています。