私がRemixを愛する理由
kentcdodds.comは私(とチーム)でRemixを使って一からカスタマイズして建てたウェブサイトです。このフレームワークを使って何万行ものコードを書いていくうちに、このフレームワークが私や私のブログのユーザーに与えるものに感謝の気持ちを持つようになりました。その一部を皆さんに伝えたいと思います。
文中より
これがウェブサイトを建てる上でRemixを是非使いたい主な理由です:
これだけです。その心は?もう少し深掘りしてみましょう…
ユーザーエクスペリエンス
ユーザーがソフトウェアを使用する際に体験することに影響を与える要素はたくさんあります。多くの場合、性能やスピードばかりに目が行きがちだと思います。しかし、それは重要な側面の一つではあるもののたくさんある要素のうちの一つに過ぎません。ユーザーエクスペリエンスにはウェブサイトがもつその他さまざまな側面も含まれています。そのうちのいくつかです:
アクセシビリティ
性能
コンテンツのリフロー
信頼性と可用性
エラー処理
ペンディングの管理
状態管理
プログレッシブエンハンスメント
レジリエンス(劣悪なネットワーク環境下での挙動)
レイアウト
コンテンツの明瞭さ
機能を開発する速度ですらユーザーのソフトウェア使用時の体験に影響を与えます。つまり、ユーザーの体験は我々のコードの保全性に非直接的な影響を受けているのです。
Remixはこれら多くの課題の役に立っています。いくつかに至っては私があえて考える必要もなくです。特に状態管理(書き換えとデータロードの競合条件)に関わる比較的難易度の高い問題は、Remixのフレームワーク内で完全に管理されています。これにより、ユーザーは古いデータを見ていることに気づいて自らページを更新する必要がなくなります。これは私が頭を使うまでもなく実現します。それがRemixのやり方なのです。
データやアセットのプリロードを適切なタイミングで行う<Link />タグを活用することでRemixはウェブサイトの速度を保つために多くのことをしてくれます。ときどき私は自分のサイトが実際はサーバーでレンダリング/ハイドレーションされているのに、CDN上の静的ファイルのように感じることに感動します。しかもそれぞれのページはユーザーごとに固有です。(CDN上の共有HTTPキャッシュでは不可能です)
RemixのプラットフォームAPIの利用が前述したようなことを可能にします。さらに、そのプラットフォームAPIがRemixを劣悪なネットワーク環境下に強く、かつプログレッシブエンハンスメント向きなフレームワークにしています。JavaScriptのローディングが遅い、または失敗するような劣悪なネットワーク環境下で、Remixの書き換え標準API(`<Form />`)はアプリがハイドレートするより前でも実際に動きます。それはつまりユーザーは劣悪な環境下でもそのアプリを使って作業をし始められるということです。`onClick`ハンドラーをロードし終わっていない無反応なボタンよりよっぽどいいでしょう(この状況はRemix以前の私にとっての普通でした)!
Remixの宣言型エラー処理はエラーが起きた場所のコンテキストに応じて適切にエラーに対処することをより簡単にします。ネストされたルーティングと組み合わせれば、アプリの他の部分を壊すことなくコンテキストに沿ったエラーのレンダリングができるようになります。
また、このエラー処理はサーバー側でも動くので(これはRemix特有のです)ユーザーはクライアントの遷移の途中でも、ドキュメント全体のダウンロード中でもエラーの起きたタイミングに関わらず同じ経験をします。
コード
私が開発に携わったアプリは世界中の何百万人もの人々に使われています。自分がデプロイしたコードに心から満足できたのはRemixで開発したウェブサイトが初めてです。そのようなことが起きた最大の理由は、Remix以前はユーザーエクスペリエンスの問題を対処しようとするためだけに多大な時間を掛けていたからです。Remixがユーザーエクスペリエンスにかなり従事しているので私自身で管理するコードはそれほど複雑になりません。そのため、私がアプリを開発するのに残されたやるべきことと言えば、Remix(とReact)が提供する宣言型APIを使うことだけで、しかもユーザーエクスペリエンスは特に手を加えなくても良いのです。
正直なところ、これがコード部分に関することで最も発信したいことです。あなたもデモコードがペンディング状態、競合条件、エラー処理やアクセシビリティーなどの違いを省略しがちということに気づいていますよね?私のコードはそういうデモコードとは少し違いますが、Remixを使うと似たような印象になります。確かに私は未だにアクセシビリティ(Remixの@reach-uiパッケージが役に立ちはしますが)とエラー/ペンディング状態については頭をひねっています。しかし、Remixが提供するそれらの処理のためのAPIは簡単で宣言的です。例えば:
// app/events/$eventId/attendees.tsx
const loader: LoaderFunction = async ({request, params}) => {
// これはサーバー側で動きます
// 想定外のランタイムエラーはErrorBoundaryがレンダーされる際に誘発されます
// 想定済みエラー(401や404など)はCatchBoundaryでレンダーされます
// それ以外では、レスポンスを返すことができ、デフォルトのコンポーネントがレンダーされます
return json(data)
}
export default function AttendeesRoute() {
const data = useLoaderData()
return <div>{/* データをレンダー */}</div>
}
export function ErrorBoundary({error}) {
return <div>{/* "想定外のエラー"のメッセージをレンダー */}</div>
}
export function CatchBoundary() {
const caught = useCatch()
return <div>{/* 400ステータスレベルのエラーのレスポンスをレンダー */}</div>
}
そして、ペンディング状態は(書き換えか通常の遷移に関わらず)どこでもペンディングUIを表示したい場所(グローバル、ローカル問わず)でこれを使えます。
const transition = useTransition()
const text =
transition.state === 'submitting'
? 'Saving...'
: transition.state === 'loading'
? 'Saved!'
: 'Ready'
これにより、ここで示すReactコードはHTTP関連のReactコードを一切書かない、劇的に簡単なものになりました。このクライアント/サーバー間のコミュニケーションはRemixが完全に管理しており、さらにユーザーエクスペリエンスに最適化する管理方法をとっています。忘れてはいけないのが、そのクライアント/サーバー間の境界も完全に型付け可能なので、ブラウザとエディターの間を行ったり来たりしてくだらないミスを修正をするのに掛ける時間を減らせることです。
私はRemixがウェブAPIを基盤にしていることも、非常に気に入っています。loaderの中で呼んでいる`json`関数は何か?それは標準的な`Response`オブジェクトを作るただの関数です。その通り。Remixで何かをするために学習する場合、Remixの公式ドキュメントと同じ(かそれ以上の)時間をmdnに費やすこととなるでしょう。そしてそれが私がRemixを愛するもう一つの理由です。
これも、RemixがWebプラットフォームを多用し、可能な限りウェブプラットフォームAPIを使っているからです。(これはReactを使いこなせるようになればなるほどJavaScriptが得意になる感覚と似ています。)
そして、Remixはサーバーの共通インターフェースとしてWeb APIを基盤としているため同じアプリをどんなプラットフォームにもデプロイでき(持参するコードがそのプラットフォーム上で動くことが条件)、あなたがやるべきとこと言えば、使うアダプターの変更のみです。サーバーレスで動かそうが、Dockerコンテナで動かそうが、Remixなら大丈夫です。
`loader`関連のもう一つの素晴らしいところは、それがサーバーで動作するため、必要以上に膨大なデータを提供するAPIを叩いて、必要な部分だけに絞れることです。これが意味することは、多くの人が複雑な`graphql`に手を出すこととなったデータのオーバーフェッチ問題を自然に解消できるということです。私が言いたいのは、`graphql`を使うことは一向に構わないですが、Remixがクライアント/サーバー間のコミュニケーションを管理してくれるので、膨大で複雑な`graphql`のクライアントライブラリをブラウザに送る必要はなく、代わりにRemixが適切なタイミングで適切に処理してくれるのに頼ればいいということです(実際そうです)。
サーバーがクライアント側に送ってくるデータが不十分というときは、ファイルをスクロールアップして、loaderを必要な追加データを含むように書き換えるだけですぐに利用できます。全て型付きでクライアントサイドのコードですぐに使えるようになります。これは素晴らしいことです。
前のセクションで`<Form />`コンポーネントとそれがどのようにしてJavaScriptがロードされる前でも動くのかということについて触れました。それが理由でRemixはユーザーエクスペリエンスに優れています。さらに、書き換えのための大量の`fetch`と実際の書き換えには無意味な状態の管理をする必要がなくなるのでRemixは開発者にとっても便利なものと言えます。通常、`onSubmit`は`event.preventDefault()`と`fetch`と競合条件の管理と無効なコードのキャッシュを含みます。ところがRemixではそういうものは一切なく、宣言型書き換えAPIだけが残ります。
// app/events/$eventId/attendees.tsx
const action: ActionFunction = async ({request}) => {
// これはサーバー側で動き、データベースと直接やりとりする場合でも、
// 実際の書き換え操作のための下流のサービスの呼びだしでも、いつでもここで
// リクエストフォームを操作できます。見事なものです。
return redirect(/* これ以後送りたい時にユーザーに送信します */)
}
export default function AttendeesRoute() {
// ほら見て!イベントハンドラーもuseEffectも必要ありません!
// 競合条件は処理されてます。
return (
<Form method="post">
<div>
<label htmlFor="name-input">Name: </label>
<input id="name-input" name="name" />
</div>
<div>
<label htmlFor="email-input">Email: </label>
<input id="email-input" name="email" type="email" />
</div>
<button type="submit">Add Attendee</button>
</Form>
)
}
検証も追加したいと思いますよね?その場合、サーバー側でやりたかったらサーバー側に`action`を追加することになります。でもクライアント側にも欲しいと思いますよね?それなら、文字通りその検証ロジックを`action`から関数に移動させ、その関数を`action`とコンポーネントの両方で呼べばいいのです。それだけです。なんて素晴らしい。
結論
この話は続けようと思えばいくらでも続けられます。私の頭の中にはたくさんのブログ記事とワークショップがあるのです。Remixを使った楽観的UIの実装がいかに簡単か、安全な認証、抽象化(コードの再利用)、ページ付け、ネストされたルーティングのおかげで`<Layout />`コンポーネントが不要なこと、などなど、話しきれなかったことがたくさんあります。いずれはすべて紹介したいと思います。
最後の締めくくりにここに立ち返りたいと思います: