Remix Tutorialをやる
やること
Remix Tutorialやります
Remixはほぼ初見なのでTutorialをやる中でもドキュメントにも随時触れていくので、
こまめに区切りつつ記録する。
Setup
setupからrunまで
まずはプロジェクトのsetupを行う。
npx create-remix@latest --template remix-run/remix/templates/remix-tutorial
実際には下記のように進む。
- ディレクトリ名を決めて
- git のセットアップをするか聞かれて
- npmがrecommendなのでそれで良い?と聞かれる
2, 3はいずれもyesで通した状態。
ここから npm run devすると即座にサーバが立ち上がってページ表示可能。
通常のプロジェクトセットアップについて
通常のsetupにおいてはQuick Startをみると良い。
他のTemplateも下記に集約されているみたい。
The Root Route
通常はここにグローバルレイアウトを定義することになるとのこと。
基本的には app/root.tsx
がその扱いになる理解で良い?のかも。
ドキュメントを見ていても特筆することはなく、「<Outlet>
が他のページをレンダリングする」ということ以上には説明もなさそうなので、
app/root.tsx
がベースのルートとなっているくらいの認識でとりあえずはよさそう。
links
Adding Stylesheets with
stylesheetをlink要素として適用するための方法。
いくつかの方法があるらしいが、下記の手法を扱っている。
これってどういう仕組みだろう...と思ってみているが、チュートリアルの下記より
Every route can export a links function. They will be collected and rendered into the <Links /> component we rendered in app/root.tsx.
とあるので、links
という名称でどのファイルからでもexportしたら適用対象になるということ?
もしもこれが LinksFunction
な配列を返す関数でなければどうなるのだろう?
下記のようにしてみる
// export const links: LinksFunction = () => [
// { rel: "stylesheet", href: appStylesHref },
// ];
export const links = "foo";
hmrでエラーになった。
関数じゃないよ、と。
関数にしたらどうなるんでしょう。
// export const links: LinksFunction = () => [
// { rel: "stylesheet", href: appStylesHref },
// ];
export const links = () => "foo";
これだとレンダリングされた。
link要素がどうなってるかをみてみる。
空のlink要素として出力されている。
基本的にはちゃんと定義できていなかったらビルドでこけるだろうし、関数が返す内容に異常があればフォールバックとしては問題なさそうにも思う。
ここで抑えておきたい注意事項は、links
という命名でのnamed exportはやめておこうということくらいかも。
The Contact Route UI
Remixでrouteを作るときの内容
下記にRemixのroute作成ルールがある
おおむねまとめると下記の認識で良さそう
-
app/routes
配下にコンポーネントファイルを作成する - pathの階層を掘る場合はドット(
.
)区切り - 動的セグメントを作る場合は先頭に
$
※アンダースコアの話も絡んできそうなところだが、これはレイアウトをどうするかの話で出てきそうなのでここでは割愛
tutorialにもある例だと、下記のようなルートを作成したい場合は、
/contacts/123
/contacts/abc
app/routes/contacts.$contactId.tsx
を作成することになる。
Nested Routes and outlets
RemixはReact Router上に構築されているため、ネストされたルーティングをサポートしているよとのこと。
ふーん...? (React Routerに疎い かつNested Routesのことがよくわかってない)
こっちも読んだ方が良さそうですね
ついでにこちらも。こっちの方がなんとなくイメージはつかめた。
で、Remixでこれを実現するにはどうするのって話。
=> <Outlet />
っていうremixが提供するコンポーネントを使うと良いよ!とのこと。
確かにこれで app/routes/contacts.$contactId.tsx
で定義した内容がレンダリングされてきた。
正直ヨクワカラン
Outletってなんですかというところから。
Renders the matching child route of a parent route.
親ルートに一致する子ルートをレンダリングする...?
よくわかんねえとなったけど、さっき雰囲気で読んでたこちらの記事やReact RouterのOutletとはに答えは書いてあった。
そもそもReact Routerの機能なんですね Outletって。
で、今回の場合はルートルート(root Route)にOutletを置いたわけだけど、
その子コンポーネント(ページと捉えた方が良さそう)に一致するパスがURLとなっているときに、そのコンポーネントを表示するよっていう宣言のコンポーネントだという理解。
Loading Data
データロードの話。
サンプルの通りにやってみること自体は良いが、まあよくわからない。
ポイントになりそうなのは2点 + 1つ気になることがある。
-
loader
について -
useLoaderData
について -
json
関数について
loaderについて
非同期関数を定義してnamed exportしただけ。
ちょっと前に出てきたlinksのようにRemixにおける特別扱いなものなのかも。
で、ドキュメント。
重要なのは下記かな。
- サーバサイドの呼び出しとして行う
- loader非同期関数の中で利用された情報はコンパイラによってブラウザバンドルから削除される
- loaderが返した情報はクライアントに公開される (コンポーネントがレンダリングされてない場合でも。)
Next.js Page RouterにおけるgetServerSideProps等と似た理解で良いかも。返す情報にはもちろん注意する。
useLoaderData
最も近いloaderからシリアライズされたデータを返す関数。
それ以上の話は上記にはない。
loader関数では json
という関数でシリアライズしてデータを返しているということだろう。
コンポーネントの中でloaderが返したデータを扱う場合にはuseLoaderDataを使おうねという理解で良さそう。
loaderとuseLoaderDataの関係性については下記も読んでおくと良さそうだ。
json
忘れてはならないこの関数。
application/json
フォーマットを返すショートカットの関数。
loaderがシリアライズされたデータを返してあげる必要があるので、そのためのものだろう。
statusコードやheadersも指定が可能らしい。
====
途中で見てた参照記事なども踏まえて、remixにおけるデータの取り扱いや状態管理はこの一連の内容で行うということを覚えておけると良さそう。
Type Interface
前節のuseLoaderDataの取り扱いで型エラー出てるなあと思って、他のドキュメント見て直したけど、その説明だった。
これもRemixのデータ取り扱いでは基本になりそう。
ざっくりいうと、loader関数をtypeofしたものを型引数として渡そうねで良さそう。
const loader = async () => {
...
return json({ ... })
}
.
.
const data = await useLoaderData<typeof loader>()
URL Params in Loaders
URLのパラメータを用いたデータアクセス。
先ほどやったloader関数に対して、params
を渡して動的パラメータへのアクセスが可能とのこと。
この辺りはloaderのドキュメントに説明がある。他にもrequest
やcontext
にもアクセスが可能そう。
Validating Params and Throwing Responses
loaderでのパラメータの検証を行なう話。
ここではinvariant
(tiny-invariant)を用いてパラメータの検証を行う。
これによりパラメータの存在チェックができる。
その後loader関数でデータを取得した後に、null可能性があるためコンポーネント側で検証が必要になるが、
これをloader側で値の存在確認をして、存在しない場合は throw new Response("Not Found", { status: 404 });
で404を返す。
この時のResponse
はFetch APIのResponseを返している。
Data Mutations
データ更新のためのformについての話。
基本的には読み物になっているが、気になるのは下記
Without client side routing, the browser will serialize the form's data automatically and send it to the server as the request body for POST, and as URLSearchParams for GET. Remix does the same thing, except instead of sending the request to the server, it uses client side routing and sends it to the route's action function.
上記引用の前段に「フォームはナビゲーションを引き起こし〜」という話がある中で、
通常のFormではGETの場合はURLSearchParamsとして、POSTの場合はリクエストボディとしてサーバに送信するとあるが、
Remixにおいてはこれをサーバ送信するのではなく、クライアントサイドルーティングをした上でrouteのaction関数に送信するよ ということらしい。
これが次のchapter以降で説明されていきそう。
Creating Contacts
情報作成のため、まずはroot Routeでaction関数を作成する。
loaderと構造自体はほぼ同じ感じに思える。
動作の流れ的には
- Form送信
- ルートに対応するactionがデータを受け取る
- Remixがaction処理後に自動的に再検証を行う
- これでユーザーからすればデータの作成とともにページの更新が行われる
といったことらしい。
HTMLとHTTPでの動作なので、JSを無効にしても動くとのこと。
疑問メモ
- 1ページにおけるactionが複数になるケースはないものか?
- root routeにactionを作成したが、actionはどの単位で持てるもの?
(なんか2つとも類似の疑問だが...)
改めてドキュメントを眺めつつ、
A route action is a server only function to handle data mutations and other actions.
とあるので、route単位でactionは持てそうな気がする。
1ページで複数actionあったりする時どうするのって話は、多分呼び分ける方法があるのか、もしくはリクエスト情報から何らか分岐させるとかになるのかな。
ここではまだ想像の領域。
Updating Data
いきなり変わったファイル名をまた作ることになる。
ドキュメントとして参照した方が良いのは下記。
例えば contacts.$contactId.tsx
と contacts.$contactId.edit.tsx
だと、パス的には下記のようになる
-
contacts.$contactId.tsx
:/contacts/{contactId}
-
contacts.$contactId.edit.tsx
:/contacts/{contactId}/edit
後者のパスに関しては一番近いパスとなる前者のパスにネストされたルートになって、
レイアウトのネストが発生することになる。
このレイアウトのネストを起こしたくない場合に、直前の(?)パスにおいてアンダースコア_
をつけるとネストを回避できるというもの。
この場合は app/root.tsx
にネストされるらしい。
ここのChapterでは更新用のルートとフォームの作成のみとなっており、実際の更新処理は次みたい。
FormData
Updating Contacts with
先ほど作成したルートにおけるフォームでは、まだactionの処理が実装されてないので、それを実装している。
Creating Contactsで実装したようなactionと特にやることは変わりないので特になし。
Mutation Discussion
どういう仕組みでフォームデータが送信されているか・処理しているのかの説明。
action関数とredirect関数を除けば、formDataなどは通常のWebとしての機能で、Remixが特別に提供しているものではないよという説明がある。
普段のformの取り扱いはライブラリ依存なこともしばしばあるが、こういう点では個人的にも好感が持てる。
(いわゆるWeb技術に直接触れることができている感じで、何しているかよくわからんが...という気持ちは少し減る)
===
ここまででデータの取得、作成や更新などを見てきたが、loader, actionが基本的に把握できていれば取り回しは非常にやりやすそうな気がする。
シンプルだなとも感じる。
Redirecting new records to the edit page
リダイレクトを行う方法がわかったので〜というところで、Creating Contactsのchapterで準備したレコード作成用のactionにリダイレクト処理を作成して、即座に情報の編集画面を開くようにしましょうというもの。
redirect
関数を上記処理に入れ込んでいくだけ。
Active Link Styling
サイドバーに表示しているcontactsから、今ページでアクティブなものをスタイリングしようよというもの。
remixが提供しているNavLink
コンポーネントを扱うことで実装できるよというもの。
(ここまでのチュートリアルではLink
コンポーネントを使っていたが、それを置き換えるイメージ)
Link
ではダメで、NavLink
だとこういうことができるのは、
NavLink
の場合はto
にマッチするルートである場合はアクティブかどうかを判断してくれて、
かつclassName
にコールバック関数を渡すことができて、その中でアクティブ判定を元にスタイルを決めることができるようになっているためである。
Linkでも勿論個別に関数を書けば実現できるだろうが、ビルトインでこういうコンポーネントがあるなら活用する形で良さそう。
Global Pending UI
As the user navigates the app, Remix will leave the old page up as data is loading for the next page.
とあり、要はロード中の表示を制御するUIを実装しようというもの。
useNavigation
フックを使えば、現在のナビゲーションの状態を返してくれて、
ここのtutorialでは画面のロード状態の場合にはフェードがかかるようにしている。
useNavigation: https://remix.run/docs/ja/main/hooks/use-navigation
Nextでいうloadingみたいなことだろうな。(実装の仕方は全く異なるが)
Deleting Records
まだやってなかったレコードの削除処理の実装。
この時点で、削除ボタンはあるもののその先の処理は未実装なのでそれをどう処理するかという状態。
すでにapp/routes/contacts.$contactId.tsx
で用意されている削除ボタンのFormでは
action="destroy"
と指定がある。この場合は相対パスでdestroy
のルートに対して処理が行われるので、
- 新ルートとして、相対パスである
app/routes/contacts.$contactId.destroy.tsx
を作成 - そのルートで削除のためのaction関数を用意する必要がある
- この時、actionで即座にredirectを行う
のようなことを実施する必要がある。
Index Routes
ルートページの場合、Outletである箇所には何も一致する子コンポーネントがないので、空白がある
これをデフォルト表示で埋めたいよね!という話(?)
app/routes/_index.tsx
を作成して、そこでコンポーネントを定義する。
_index.tsx
は特別なファイルで、親ルートと一致するパスにいる場合に表示されるコンポーネントであって、この時Outletに表示するコンポーネントはないもの、という理解で良さそう (正直うまく説明できない感じ)
Cancel Button
キャンセルボタンの実装。
ここではブラウザの戻るボタン相当の機能を実装するとある。
ここでもuseNavigateを使うことになり、ヒストリーバックをしている。
参考: https://remix.run/docs/en/main/hooks/use-navigate#to-number
URLSearchParams
and GET
Submissions
ここまでのフォーム送信はいずれもPOSTによるaction関数への送信処理のみだったが、
ここにきてGETによるフォーム送信 (この時の送信情報はURLSearchParamsというのは結構前のchapterで説明があった気がする)。
GETのフォーム送信ではPOSTのようにaction関数と紐づいているみたいなイメージよりは、
通常のGETリクエスト(e.g. リンクの遷移等) と同じ扱いになるので、
loader
関数での処理になる。
Synchronizing URLs to Form State
GETフォームでは検索ボックスでの処理を想定した実装を行なったが、これだけでは不十分で、
例えばヒストリーバックした場合にテキストボックスに入力文字列の状態が残っていたり、
逆にリロードすると結果はフィルタされていても、テキストボックスには何も値がなかったり、
URLの状態と一致していない画面の状態となっている。
とはいえここでやっていることとしては、loaderで、検索結果の情報に加えてクエリの情報を返すのみで、
それをテキストボックスのデフォルト値に設定しているというもの。
これは通常のUX体験としての実装なので、そこまでRemixによった実装というものでもないかも。
(それでもloader使うじゃんというのはあるが、感覚的な問題ですかねえ。。)
ただ、これだけでは下記の同期はできないので、ここで useEffect
を使ってDOMに対しての値設定で同期を行っている。
ヒストリーバックした場合にテキストボックスに入力文字列の状態が残っていたり
Form
's onChange
Submitting
ボタンによるsubmitイベントでの検索をしていたが、onChangeイベントでもできるようにしましょうというもの。
useSubmit
フックを使って、FormのonChangeイベントにて値の送信を行なうだけ。
Adding Search Spinner
検索フォームなどの場合に、検索中であることをスピナーコンポーネントで知らせてあげようねというもの。
ここではuseNavigate
フックを使って、navigationの状態からスピナーの表示・非表示を制御している。
こんな条件を追加した。
const searching =
navigation.location &&
new URLSearchParams(navigation.location.search).has(
"q"
);
navigation.location.search
というのがいきなり出てくるけどなんでしょうな。
In the case of a GET form submission, formData will be empty and the data will be reflected in navigation.location.search.
search は Location インターフェイスのプロパティで、クエリー文字列とも呼ばれる検索文字列です。つまり、 '?' の後の URL 引数を含む文字列を指定します。
要はクエリパラメータのことか。
なので追加した条件は「ナビゲーションが発生していて、かつqのクエリでの検索が発生している」という条件になるのかな。
Managing the History Stack
さっき実装したFormでのonChangeハンドラでのsubmitに関連。
これによって都度キーストロークごとに送信が行われるので、膨大な履歴が溜まっちゃうよ!というのを回避する実装をしたいというもの。
ここでは、「テキストボックスの入力が空である場合は初めての入力として、初めての入力でない(=すでに入力値が存在する場合) は、履歴をpushするのではなく、現在のエントリと置き換える」ということをしている。
useSubmitのreplace
オプションが参考になる。
replace: Replaces the current entry in the history stack, instead of pushing the new entry. Default is false.
Forms Without Navigation
ナビゲーションを発生させずにフォーム送信を行いたいというニーズに対応する機能。
同一ページ内でのデータ変更である場合に利用することになりそう。
useFetcher
フックがこれを実現する。
useFetcher
を呼び出して、<fetcher.Form ...></fetcher.Form>
のようにするだけでいいらしい。
データの再検証等も行なってくれるので、本当にナビゲーションのみを除外したフォームということになりそうだ。
Optimistic UI
サンプルアプリケーションでは実際のデータ処理も考慮して、処理時にダミー的に処理に時間がかかるようにしてある。
先ほど実装したナビゲーションなしのフォーム処理では、⭐️をつけるか否かの処理だった。
この時点では、フォーム送信後に状態が反映されるまで時間がかかったが、楽観的UI (optimistic UI)として、みなし的に画面の状態を更新するようにしても良いのではないかという提案である。
ここでは、
「fetcherが持つformDataの状態を確認できる場合(=データ送信処理後)は、その状態を元にコンポーネントを再レンダリングさせて、そうでない場合は実際の状態に合わせてコンポーネントをレンダリングする」ということを行なっている。
これでtutorialとしては以上。
Discussion Topics も面白そうというか、おそらく開発を進める上では目を通しておいた方が良さそうなので、これは順次読み進めていく。
Tutorialとしてのスクラップだったので、こちらはクローズする