Remix チュートリアル
連絡先を管理できる、小さくても機能豊富なアプリを作成します。データベースやその他の「本番環境対応」のものは使用しないため、Remix に集中できます。一緒に作業すれば約30分かかりますが、そうでなければ簡単に読めます。
👉 これが表示されたら、アプリで何か操作する必要があります!
残りは情報とより深い理解のためです。始めましょう。
セットアップ
👉 基本テンプレートの生成
これは非常にシンプルなテンプレートを使用しますが、CSSとデータモデルが含まれているため、Remixに集中できます。より詳しく知りたい場合は、クイックスタートでRemixプロジェクトの基本的なセットアップについて学ぶことができます。
👉 アプリの起動
http://localhost:5173 を開くと、次のようなスタイルのない画面が表示されます。
ルートルート
app/root.tsx
のファイルに注目してください。これは「ルートルート」と呼んでいます。UIで最初にレンダリングされるコンポーネントなので、通常はページのグローバルレイアウトが含まれています。
ルートコンポーネントのコードを見るにはここをクリック
links
を使用したスタイルシートの追加
Remix アプリをスタイリングする方法は複数ありますが、Remix に焦点を当てるために、既に記述済みのプレーンなスタイルシートを使用します。
CSS ファイルは JavaScript モジュールに直接インポートできます。Vite はアセットのフィンガープリントを作成し、ビルドのクライアントディレクトリに保存し、公開アクセス可能な href をモジュールに提供します。
👉 アプリスタイルのインポート
すべてのルートは links
関数をエクスポートできます。それらは収集され、app/root.tsx
でレンダリングした <Links />
コンポーネントにレンダリングされます。
アプリはこれで次のようになります。CSSも書けるデザイナーがいるのは本当に素晴らしいですね!(Jimさん、ありがとうございます 🙏)。
コンタクトルートUI
サイドバーのアイテムをクリックすると、デフォルトの404ページが表示されます。/contacts/1
というURLに一致するルートを作成しましょう。
👉 app/routes
ディレクトリとコンタクトルートモジュールの作成
Remixのルートファイルの命名規則では、.
はURLに/
を作成し、$
はセグメントを動的にします。これにより、次のようなURLに一致するルートが作成されました。
/contacts/123
/contacts/abc
👉 コンタクトコンポーネントUIの追加
いくつかの要素だけです。コピー&ペーストして構いません。
リンクをクリックするか/contacts/1
にアクセスしても、何も変わりませんか?
ネストされたルートとアウトレット
RemixはReact Router上に構築されているため、ネストされたルーティングをサポートしています。子ルートを親レイアウト内でレンダリングするには、親にOutlet
をレンダリングする必要があります。これを修正するために、app/root.tsx
を開いて、アウトレットをレンダリングしましょう。
👉 <Outlet />
をレンダリングする
これで、子ルートがアウトレットを通してレンダリングされるはずです。
クライアントサイドルーティング
気づかれたかもしれませんが、サイドバーのリンクをクリックすると、ブラウザはクライアントサイドルーティングではなく、次のURLに対して完全なドキュメントリクエストを行っています。
クライアントサイドルーティングにより、アプリはサーバーから別のドキュメントをリクエストすることなく、URLを更新できます。代わりに、アプリはすぐに新しいUIをレンダリングできます。<Link>
を使って実現しましょう。
👉 サイドバーの<a href>
を<Link to>
に変更してください
ブラウザの開発ツールでネットワークタブを開くと、ドキュメントをリクエストしなくなっていることが確認できます。
データの読み込み
URL セグメント、レイアウト、データは、ほとんどの場合、互いに関連付けられています(3つ組?)。このアプリでも既に確認できます。
URL セグメント | コンポーネント | データ |
---|
/ | <Root> | 連絡先のリスト |
contacts/:contactId | <Contact> | 個々の連絡先 |
この自然な結合のため、Remix には、ルートコンポーネントに簡単にデータを取得するためのデータ規則があります。
データを読み込むために使用する2つのAPI、loader
とuseLoaderData
があります。 まず、ルートルートにloader
関数を生成してエクスポートし、データをレンダリングします。
👉 app/root.tsx
からloader
関数をエクスポートしてデータをレンダリングする
次のコードには型エラーが含まれています。これは次のセクションで修正します。
以上です!Remix はこれで、データをUIと自動的に同期状態に保ちます。サイドバーは次のようになります。
型推論
map
内の contact
型について、TypeScript が警告を出していることに気づかれたかもしれません。typeof loader
を使用してデータに関する型推論を行うための簡単なアノテーションを追加できます。
ローダーにおけるURLパラメータ
👉 サイドバーのリンクのいずれかをクリックしてください
以前の静的なコンタクトページが表示されるはずです。違いは、URLにレコードの実際のIDが含まれるようになったことです。
app/routes/contacts.$contactId.tsx
のファイル名にある$contactId
の部分を覚えていますか?これらの動的なセグメントは、URLのその位置にある動的な(変化する)値と一致します。URL内のこれらの値を「URLパラメータ」、または単に「パラメータ」と呼びます。
これらのparams
は、動的セグメントと一致するキーを使用してローダーに渡されます。たとえば、セグメントの名前が$contactId
なので、値はparams.contactId
として渡されます。
これらのパラメータは、多くの場合、IDでレコードを見つけるために使用されます。試してみましょう。
👉 コンタクトページにloader
関数を追加し、useLoaderData
でデータにアクセスします
次のコードには型エラーが含まれています。次のセクションで修正します。
パラメータの検証とレスポンスの送出
TypeScript が怒っています。TypeScript を満足させ、それがどのような考慮事項を強制するかを見てみましょう。
まず、ファイル名とコード間でパラメータの名前を間違えた可能性があることがわかります(ファイル名を変更したのかもしれません!)。invariant
は、コードの潜在的な問題を予測したときに、カスタムメッセージ付きのエラーをスローするための便利な関数です。
次に、useLoaderData<typeof loader>()
は、コンタクトを取得したか、null
を取得したか(その ID のコンタクトが存在しない可能性があります)を知るようになりました。この潜在的な null
はコンポーネントコードにとって面倒であり、TypeScript のエラーはまだ飛び回っています。
コンポーネントコードでコンタクトが見つからない可能性を考慮することもできますが、Web 的な方法は適切な 404 を送信することです。ローダーでこれを行うことで、すべての問題を一度に解決できます。
これで、ユーザーが見つからない場合、このパスでのコード実行は停止し、Remix は代わりにエラーパスをレンダリングします。Remix のコンポーネントは、ハッピーパスにのみ集中できます 😁
データの変更
最初のコンタクトをすぐに作成しますが、まずはHTMLについて説明しましょう。
Remixは、データ変更のプリミティブとしてHTMLフォームのナビゲーションをエミュレートします。これは、JavaScriptのカンブリア爆発以前は唯一の方法でした。そのシンプルさに騙されてはいけません!Remixのフォームは、クライアントサイドレンダリングアプリのUX機能を、「旧来型」のウェブモデルのシンプルさで実現します。
一部のWeb開発者には馴染みがありませんが、HTMLのform
は、リンクをクリックするのと同じように、ブラウザ内でナビゲーションを引き起こします。唯一の違いはリクエストにあります。リンクはURLのみを変更できますが、form
はリクエストメソッド(GET
とPOST
)とリクエストボディ(POST
フォームデータ)も変更できます。
クライアントサイドルーティングがない場合、ブラウザはform
のデータを自動的にシリアライズし、POST
のリクエストボディとして、GET
の場合はURLSearchParamsとしてサーバーに送信します。Remixも同じことを行いますが、サーバーにリクエストを送信する代わりに、クライアントサイドルーティングを使用して、ルートのaction関数に送信します。
アプリの「新規」ボタンをクリックして、これを試すことができます。
Remixは、このフォームナビゲーションを処理するサーバー側のコードがないため、405エラーを送信します。
url-search-params: URLSearchParamsの説明へのリンク (URLSearchParamsの説明へのリンクをここに挿入してください)
action: action関数の説明へのリンク (action関数の説明へのリンクをここに挿入してください)
連絡先の作成
ルートルートからaction
関数をエクスポートすることで、新しい連絡先を作成します。ユーザーが「新規」ボタンをクリックすると、フォームはルートルートアクションにPOST
を送信します。
👉 app/root.tsx
からaction
関数をエクスポートする
以上です!「新規」ボタンをクリックしてみてください。新しいレコードがリストに追加されるはずです🥳
createEmptyContact
メソッドは、名前やデータのない空の連絡先を作成するだけです。しかし、レコードは作成されます!
🧐ちょっと待って…サイドバーはどうやって更新されたのですか?action
関数はどこで呼び出されたのですか?データを再取得するコードはどこにあるのですか?useState
、onSubmit
、useEffect
はどこにあるのですか?!
ここで「旧来のウェブ」プログラミングモデルが登場します。<Form>
は、ブラウザがサーバーへのリクエストを送信するのを防ぎ、代わりにfetch
を使用してルートのaction
関数に送信します。
ウェブセマンティクスでは、POST
は通常、データが変更されていることを意味します。慣例により、Remixはこれをヒントとして使用し、action
が完了した後にページのデータを自動的に再検証します。
実際、すべてがHTMLとHTTPであるため、JavaScriptを無効にしても、すべて正常に動作します。Remixがフォームをシリアル化してサーバーにfetch
リクエストを行う代わりに、ブラウザがフォームをシリアル化してドキュメントリクエストを行います。そこからRemixはサーバーサイドでページをレンダリングし、送信します。最終的には同じUIです。
しかし、ファビコンの回転や静的なドキュメントよりも優れたユーザーエクスペリエンスを作成するため、JavaScriptは使用し続けます。
データの更新
新しいレコードの情報を入力する方法を追加しましょう。
データの作成と同様に、<Form>
を使用してデータを更新します。app/routes/contacts.$contactId_.edit.tsx
に新しいルートを作成しましょう。
👉 編集コンポーネントの作成
$contactId_
の奇妙な _
に注目してください。デフォルトでは、ルートは同じプレフィックス名のルート内に自動的にネストされます。末尾に _
を追加すると、ルートが app/routes/contacts.$contactId.tsx
内にネストされません。詳細はルートファイルの命名規則ガイドを参照してください。
👉 編集ページUIの追加
これまでに見たことのないものはありません。コピー&ペーストして自由に使用してください。
これで、新しいレコードをクリックし、「編集」ボタンをクリックすると、新しいルートが表示されます。
作成したばかりの編集ルートは既にform
をレンダリングしています。必要なのはaction
関数を追加することだけです。Remixはform
をシリアライズし、fetch
を使ってPOST
し、自動的にすべてのデータを再検証します。
👉 編集ルートにaction
関数を追加する
フォームに記入して保存ボタンを押すと、このような表示になります!(ただし、見やすく、そしておそらく毛深い部分は少なくなっています。)
Mutationに関する議論
😑 動いたけど、何が起こっているのか全く分からない…
少し詳しく見ていきましょう…
contacts.$contactId_.edit.tsx
を開き、「form」要素を見てください。それぞれにname
属性があることに注目してください。
JavaScriptがない場合、フォームが送信されると、ブラウザはFormData
を作成し、それをサーバーに送信する際の要求の本文として設定します。前に述べたように、Remixはそれを防ぎ、代わりにfetch
を使用して要求をaction
関数に送信することでブラウザをエミュレートし、FormData
を含めます。
form
内の各フィールドは、formData.get(name)
でアクセスできます。例えば、上記の入力フィールドの場合、名と姓にはこのようにアクセスできます。
フォームフィールドがいくつかあるので、Object.fromEntries
を使用してそれらをすべてオブジェクトに収集しました。これはまさにupdateContact
関数が求めているものです。
action
関数以外では、ここで説明しているAPIはRemixによって提供されているものではありません。request
、request.formData
、Object.fromEntries
はすべてWebプラットフォームによって提供されています。
action
関数が完了した後、最後にあるredirect
に注目してください。
action
関数とloader
関数の両方が Response
を返すことができます(Request
を受け取るので当然です!)。redirect
ヘルパーは、アプリに場所の変更を指示するResponse
を返すことを容易にするだけです。
クライアントサイドルーティングがない場合、POST
リクエスト後にサーバーがリダイレクトすると、新しいページは最新のデータを取得してレンダリングします。前に学んだように、Remixはこのモデルをエミュレートし、action
呼び出し後にページのデータを自動的に再検証します。そのため、フォームを保存するとサイドバーが自動的に更新されます。クライアントサイドルーティングがないと、追加の再検証コードは存在しないため、Remixではクライアントサイドルーティングがあっても存在する必要はありません!
最後に1点。JavaScriptがない場合、redirect
は通常のredirectになります。しかし、JavaScriptを使用すると、クライアントサイドのリダイレクトになるため、スクロール位置やコンポーネントの状態などのクライアントの状態が失われることはありません。
新規レコードを編集ページにリダイレクトする
リダイレクトの方法が分かったところで、新規連絡先を作成するアクションを編集ページにリダイレクトするように更新しましょう。
👉 新規レコードの編集ページにリダイレクトする
これで、「新規」をクリックすると、編集ページに移動するはずです。
アクティブリンクのスタイル設定
多くのレコードがあるため、サイドバーで現在見ているレコードが分かりにくくなっています。これを修正するためにNavLink
を使用できます。
👉 サイドバーで<Link>
を<NavLink>
に置き換えてください
className
に関数を渡していることに注意してください。ユーザーが<NavLink to>
と一致するURLにいる場合、isActive
はtrue
になります。アクティブになる直前(データの読み込み中)の場合、isPending
はtrue
になります。これにより、ユーザーの現在位置を簡単に示し、リンクをクリックしたときにデータの読み込みが必要な場合にもすぐにフィードバックを提供できます。
グローバルなPending UI
ユーザーがアプリを操作する際、Remixは次のページのデータが読み込まれている間、古いページを表示したままにします。リスト間をクリックすると、アプリが少し反応しなくなっていることに気づいたかもしれません。アプリが反応しないように感じさせないように、ユーザーにフィードバックを提供しましょう。
Remixはバックグラウンドで全ての状態を管理し、動的なウェブアプリを構築するために必要な部分を公開します。この場合、useNavigation
フックを使用します。
👉 useNavigation
を使用してグローバルなPending UIを追加する
useNavigation
は現在のナビゲーションの状態を返します。これは"idle"
、"loading"
、または"submitting"
のいずれかになります。
この場合、アイドル状態ではない場合は、アプリの主要部分に"loading"
クラスを追加します。その後、CSSによって短い遅延の後、スムーズなフェードが追加されます(高速読み込み時のUIのちらつきを防ぐため)。ただし、スピナーやトップに表示されるローディングバーなど、何でも表示できます。
レコードの削除
contact ルートのコードを確認すると、削除ボタンは次のようになっています。
action
が "destroy"
を指していることに注意してください。<Link to>
と同様に、<Form action>
は相対的な値を取ることができます。フォームは contacts.$contactId.tsx
でレンダリングされるため、destroy
という相対的なアクションは、クリックされたときにフォームを contacts.$contactId.destroy
に送信します。
この時点で、削除ボタンを動作させるために必要なことはすべて知っているはずです。先に進む前に、試してみてはいかがでしょうか?次のものが必要です。
- 新しいルート
- そのルートのアクション
app/data.ts
からの deleteContact
- どこかへのリダイレクト
👉 "destroy" ルートモジュールの作成
👉 destroy アクションの追加
さて、レコードに移動して「削除」ボタンをクリックしてください。動作します!
😅 まだなぜこれが動作するのかよく分かりません
ユーザーが送信ボタンをクリックすると:
<Form>
は、サーバーに新しいドキュメント POST
リクエストを送信するというデフォルトのブラウザの動作を阻止しますが、代わりにクライアントサイドルーティングとfetch
を使用してブラウザをエミュレートし、POST
リクエストを作成します。
<Form action="destroy">
は、contacts.$contactId_.destroy.tsx
の新しいルートと一致し、リクエストを送信します。
action
がリダイレクトされた後、Remix はページ上のデータのすべての loader
を呼び出して最新の値を取得します(これは「再検証」です)。useLoaderData
は新しい値を返し、コンポーネントを更新します!
Form
を追加し、action
を追加すると、Remix が残りの処理を行います。
インデックスルート
アプリをロードすると、リストの右側には大きな空白ページが表示されていることに気付くでしょう。
ルートに子ルートがあり、親ルートのパスにいる場合、<Outlet>
は一致する子ルートがないため、何もレンダリングしません。インデックスルートは、その空白を埋めるデフォルトの子ルートだと考えることができます。
👉 ルートルートのインデックスルートを作成する
👉 インデックスコンポーネントの要素を入力する
コピー&ペーストしていただいて構いません。特別なことはありません。
ルート名_index
は特別なものです。これはRemixに、ユーザーが親ルートの正確なパスにいる場合、このルートを一致させてレンダリングするように指示します。そのため、<Outlet />
でレンダリングする他の子ルートはありません。
できました!空白はもうありません。ダッシュボード、統計、フィードなどをインデックスルートに配置することが一般的です。これらはデータの読み込みにも参加できます。
キャンセルボタン
編集ページには、まだ何も機能していないキャンセルボタンがあります。ブラウザの戻るボタンと同じ動作をするようにしたいです。
ボタンのクリックハンドラとuseNavigate
が必要です。
👉 useNavigate
を使用してキャンセルボタンのクリックハンドラを追加する
これで、ユーザーが「キャンセル」をクリックすると、ブラウザの履歴で1つ前のエントリに戻ります。
🧐 なぜボタンにevent.preventDefault()
がないのですか?
<button type="button">
は、一見冗長に見えますが、ボタンがフォームを送信するのを防ぐHTMLの方法です。
あと2つの機能が残っています。ゴールはもうすぐです!
URLSearchParams
とGET
送信
これまでのインタラクティブなUIは、URLを変更するリンクか、action
関数にデータを送信するform
のいずれかでした。検索フィールドは両方の混合で興味深いものです。form
ですが、データは変更せず、URLのみを変更します。
検索フォームを送信したときに何が起こるか見てみましょう。
👉 検索フィールドに名前を入力してEnterキーを押してください
ブラウザのURLに、クエリがURLSearchParams
として含まれていることに注目してください。
http://localhost:5173/?q=ryan
<Form method="post">
ではないため、Remixはブラウザをエミュレートし、リクエストボディではなくFormData
をURLSearchParams
にシリアライズします。
loader
関数は、request
から検索パラメータにアクセスできます。これを利用してリストをフィルタリングしましょう。
👉 URLSearchParams
があればリストをフィルタリングする
これはGET
ではなくPOST
であるため、Remixはaction
関数を呼び出しません。GET
のform
を送信することは、リンクをクリックすることと同じです。URLだけが変更されます。
これは通常のページナビゲーションであることも意味します。戻るボタンをクリックして、元の場所に戻ることができます。
URLとフォーム状態の同期化
いくつかのUX上の問題を迅速に解決しましょう。
- 検索後、戻るボタンをクリックしても、リストがフィルタリングされなくなっても、フォームフィールドには入力した値が残っています。
- 検索後にページを更新すると、リストはフィルタリングされているにもかかわらず、フォームフィールドには値が入っていません。
つまり、URLと入力の状態が同期していません。
まず(2)を解決し、URLの値で入力を開始しましょう。
👉 loader
からq
を返し、それを入力のデフォルト値として設定します
これで、検索後にページを更新しても、入力フィールドにクエリが表示されるようになります。
次に、問題(1)、戻るボタンをクリックして入力を更新する方法についてです。ReactのuseEffect
を使用して、DOM内の入力値を直接操作できます。
👉 URLSearchParams
と入力値を同期化します
🤔 制御されたコンポーネントとReactの状態を使うべきではないでしょうか?
制御されたコンポーネントとして実装することもできます。同期するポイントが増えますが、どちらの方法でも構いません。
展開して詳細を確認
これで、戻る/進む/更新ボタンをクリックしても、入力値がURLと結果と同期するようになります。
ここで製品に関する意思決定を行う必要があります。ユーザーに結果をフィルタリングするためのform
を送信させたい場合と、ユーザーが入力する際にフィルタリングさせたい場合があります。前者は既に実装しているので、後者を見てみましょう。
useNavigate
は既に見てきましたが、今回はその仲間であるuseSubmit
を使用します。
入力すると、form
が自動的に送信されるようになりました!
submit
への引数に注目してください。submit
関数は、渡された任意のフォームをシリアライズして送信します。ここではevent.currentTarget
を渡しています。currentTarget
は、イベントがアタッチされているDOMノード(form
)です。
use-submit: https://reactrouter.com/docs/en/v6/hooks/use-submit
検索スピナーの追加
本番アプリでは、この検索は、一度にすべてを送信してクライアント側でフィルタリングするには大きすぎるデータベース内のレコードを検索している可能性が高いです。そのため、このデモでは、ネットワーク遅延を偽装しています。
ローディングインジケーターがないと、検索は少し遅く感じられます。データベースを高速化できたとしても、ユーザーのネットワーク遅延は常に存在し、制御できません。
より良いユーザーエクスペリエンスのために、検索に対する即時のUIフィードバックを追加しましょう。useNavigation
を再び使用します。
👉 検索中かどうかを知るための変数の追加
何も起こっていない場合、navigation.location
はundefined
になりますが、ユーザーがナビゲートすると、データの読み込み中に次の場所に設定されます。その後、location.search
を使用して検索中かどうかを確認します。
👉 新しいsearching
状態を使用して検索フォーム要素にクラスを追加
ボーナスとして、検索時にメイン画面をフェードアウトしないようにします。
これで、検索入力の左側に素敵なスピナーが表示されるはずです。
ヒストリスタックの管理
フォームがキーストロークごとに送信されるため、「alex」と入力してからバックスペースで削除すると、膨大なヒストリスタックが生成されます😂。これは明らかに避けなければなりません。
これを回避するには、ヒストリスタックにプッシュする代わりに、現在のエントリを次のページで置き換えることができます。
👉 submit
で replace
を使用
これが最初の検索かどうかを簡単に確認した後、置き換えるかどうかを決定します。最初の検索では新しいエントリが追加されますが、それ以降のキーストロークでは現在のエントリが置き換えられます。検索を削除するために7回戻るボタンをクリックする代わりに、ユーザーは1回戻るボタンをクリックするだけで済みます。
これまでのフォームはすべてURLを変更していました。これらのユーザーフローは一般的ですが、ナビゲーションを起こさずにフォームを送信したい場合も同様に一般的です。
このような場合、useFetcher
を使用します。これにより、ナビゲーションを起こすことなくaction
とloader
と通信できます。
連絡先ページの★ボタンはこれに適しています。新しいレコードを作成または削除するわけではなく、ページを変更する必要もありません。単に見ているページのデータを変更したいだけです。
👉 <Favorite>
フォームをfetcherフォームに変更する
このフォームは、ナビゲーションを引き起こさなくなり、単にaction
にフェッチするようになります。ところで…action
を作成するまでこれは機能しません。
👉 action
を作成する
準備完了です。ユーザー名の横にある星をクリックしましょう!
見てください、両方の星が自動的に更新されます。新しい<fetcher.Form method="post">
は、これまで使用してきた<Form>
とほぼ同じように機能します。アクションを呼び出し、その後すべてのデータが自動的に再検証されます。エラーも同様にキャッチされます。
ただし、重要な違いが1つあります。ナビゲーションではないため、URLは変更されず、履歴スタックは影響を受けません。
楽観的UI
前のセクションのお気に入りボタンをクリックした際、アプリが少し反応が鈍いと感じたかもしれません。現実世界でも発生する可能性があるため、ネットワーク遅延を追加しました。
ユーザーにフィードバックを与えるために、fetcher.state
(以前のnavigation.state
とよく似ています)を使用して、星をローディング状態にすることもできますが、今回はさらに優れた方法があります。「楽観的UI」と呼ばれる戦略を使用できます。
フェッチャーは、action
に送信されるFormData
を知っているので、fetcher.formData
で利用できます。ネットワークが完了する前でも、星の状態をすぐに更新するためにそれを使用します。更新が最終的に失敗した場合、UIは実際のデータに戻ります。
👉 fetcher.formData
から楽観的な値を読み取る
これで、星はクリックしたときにすぐに新しい状態に変わります。
以上です!Remixを試していただきありがとうございます。このチュートリアルが、優れたユーザーエクスペリエンスを構築するための堅実なスタートとなることを願っています。他にも多くのことができるので、すべてのAPIを確認してください 😀