Closed31

Remix Tutorialをやる

mm

Setup

https://remix.run/docs/ja/main/start/tutorial#setup

setupからrunまで

まずはプロジェクトのsetupを行う。

npx create-remix@latest --template remix-run/remix/templates/remix-tutorial

実際には下記のように進む。

  1. ディレクトリ名を決めて
  2. git のセットアップをするか聞かれて
  3. npmがrecommendなのでそれで良い?と聞かれる

2, 3はいずれもyesで通した状態。

ここから npm run devすると即座にサーバが立ち上がってページ表示可能。

通常のプロジェクトセットアップについて

通常のsetupにおいてはQuick Startをみると良い。
https://remix.run/docs/ja/main/start/quickstart

他のTemplateも下記に集約されているみたい。
https://remix.guide/templates

mm

The Root Route

https://remix.run/docs/ja/main/start/tutorial#the-root-route
UIでレンダリングされる最初のコンポーネントになる。
通常はここにグローバルレイアウトを定義することになるとのこと。

基本的には app/root.tsx がその扱いになる理解で良い?のかも。
https://remix.run/docs/en/main/file-conventions/routes#root-route

ドキュメントを見ていても特筆することはなく、「<Outlet>が他のページをレンダリングする」ということ以上には説明もなさそうなので、
app/root.tsxがベースのルートとなっているくらいの認識でとりあえずはよさそう。

mm

Adding Stylesheets with links

https://remix.run/docs/ja/main/start/tutorial#adding-stylesheets-with-links

stylesheetをlink要素として適用するための方法。

いくつかの方法があるらしいが、下記の手法を扱っている。
https://remix.run/docs/en/main/route/links

これってどういう仕組みだろう...と思ってみているが、チュートリアルの下記より

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はやめておこうということくらいかも。

mm

The Contact Route UI

https://remix.run/docs/ja/main/start/tutorial#the-contact-route-ui

Remixでrouteを作るときの内容

下記にRemixのroute作成ルールがある
https://remix.run/docs/ja/main/file-conventions/routes

おおむねまとめると下記の認識で良さそう

  • app/routes配下にコンポーネントファイルを作成する
  • pathの階層を掘る場合はドット(.)区切り
  • 動的セグメントを作る場合は先頭に $

※アンダースコアの話も絡んできそうなところだが、これはレイアウトをどうするかの話で出てきそうなのでここでは割愛

tutorialにもある例だと、下記のようなルートを作成したい場合は、
/contacts/123
/contacts/abc

app/routes/contacts.$contactId.tsxを作成することになる。

mm

Nested Routes and outlets

https://remix.run/docs/ja/main/start/tutorial#nested-routes-and-outlets

RemixはReact Router上に構築されているため、ネストされたルーティングをサポートしているよとのこと。

ふーん...? (React Routerに疎い かつNested Routesのことがよくわかってない)
こっちも読んだ方が良さそうですね
https://reactrouter.com/en/main/start/overview

ついでにこちらも。こっちの方がなんとなくイメージはつかめた。
https://www.robinwieruch.de/react-router-nested-routes/

で、Remixでこれを実現するにはどうするのって話。
=> <Outlet />っていうremixが提供するコンポーネントを使うと良いよ!とのこと。

確かにこれで app/routes/contacts.$contactId.tsxで定義した内容がレンダリングされてきた。
正直ヨクワカラン

Outletってなんですかというところから。
https://remix.run/docs/en/main/components/outlet

Renders the matching child route of a parent route.

親ルートに一致する子ルートをレンダリングする...?
よくわかんねえとなったけど、さっき雰囲気で読んでたこちらの記事React RouterのOutletとはに答えは書いてあった。

そもそもReact Routerの機能なんですね Outletって。

で、今回の場合はルートルート(root Route)にOutletを置いたわけだけど、
その子コンポーネント(ページと捉えた方が良さそう)に一致するパスがURLとなっているときに、そのコンポーネントを表示するよっていう宣言のコンポーネントだという理解。

mm

Client side Routing

https://remix.run/docs/ja/main/start/tutorial#client-side-routing

クライアントサイドでのルーティングの話。
今時点でのサイドバーの実装は <a href="...">...</a>なので、クライアントサイドでのルーティングではないけど、
クライアントサイドルーティングにするなら <Link>コンポーネントを使いなさいという話。

他のSSRフレームワークとかでもよくあるパターンなので、ここではふーんそうなんだという程度で良いかも。
ただ <Link>コンポーネント で出来ることも割とありそう (prefetchの挙動の制御等々) なので、実際に使うときは注意深くみても良さそう。

mm

Loading Data

https://remix.run/docs/ja/main/start/tutorial#loading-data

データロードの話。
サンプルの通りにやってみること自体は良いが、まあよくわからない。

ポイントになりそうなのは2点 + 1つ気になることがある。

  • loaderについて
  • useLoaderDataについて
  • json関数について

loaderについて

非同期関数を定義してnamed exportしただけ。
ちょっと前に出てきたlinksのようにRemixにおける特別扱いなものなのかも。

で、ドキュメント。
https://remix.run/docs/ja/main/route/loader

重要なのは下記かな。

  • サーバサイドの呼び出しとして行う
  • loader非同期関数の中で利用された情報はコンパイラによってブラウザバンドルから削除される
  • loaderが返した情報はクライアントに公開される (コンポーネントがレンダリングされてない場合でも。)

Next.js Page RouterにおけるgetServerSideProps等と似た理解で良いかも。返す情報にはもちろん注意する。

useLoaderData

https://remix.run/docs/ja/main/hooks/use-loader-data
最も近いloaderからシリアライズされたデータを返す関数。

それ以上の話は上記にはない。

loader関数では jsonという関数でシリアライズしてデータを返しているということだろう。
コンポーネントの中でloaderが返したデータを扱う場合にはuseLoaderDataを使おうねという理解で良さそう。

loaderとuseLoaderDataの関係性については下記も読んでおくと良さそうだ。
https://remix.run/docs/ja/main/discussion/data-flow

json

忘れてはならないこの関数。
https://remix.run/docs/en/main/utils/json

application/jsonフォーマットを返すショートカットの関数。
loaderがシリアライズされたデータを返してあげる必要があるので、そのためのものだろう。

statusコードやheadersも指定が可能らしい。

====
途中で見てた参照記事なども踏まえて、remixにおけるデータの取り扱いや状態管理はこの一連の内容で行うということを覚えておけると良さそう。

mm

Type Interface

https://remix.run/docs/ja/main/start/tutorial#type-inference

前節のuseLoaderDataの取り扱いで型エラー出てるなあと思って、他のドキュメント見て直したけど、その説明だった。

これもRemixのデータ取り扱いでは基本になりそう。

ざっくりいうと、loader関数をtypeofしたものを型引数として渡そうねで良さそう。

const loader = async () => {
  ...
  return json({ ... })
}
.
.
  const data = await useLoaderData<typeof loader>()
mm

Validating Params and Throwing Responses

https://remix.run/docs/ja/main/start/tutorial#validating-params-and-throwing-responses

loaderでのパラメータの検証を行なう話。

ここではinvariant (tiny-invariant)を用いてパラメータの検証を行う。
https://www.npmjs.com/package/tiny-invariant

これによりパラメータの存在チェックができる。

その後loader関数でデータを取得した後に、null可能性があるためコンポーネント側で検証が必要になるが、
これをloader側で値の存在確認をして、存在しない場合は throw new Response("Not Found", { status: 404 });で404を返す。
この時のResponseはFetch APIのResponseを返している。
https://developer.mozilla.org/ja/docs/Web/API/Response

mm

Data Mutations

https://remix.run/docs/ja/main/start/tutorial#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以降で説明されていきそう。

mm

Creating Contacts

https://remix.run/docs/ja/main/start/tutorial#creating-contacts

情報作成のため、まずはroot Routeでaction関数を作成する。
https://remix.run/docs/ja/main/route/action

loaderと構造自体はほぼ同じ感じに思える。

動作の流れ的には

  • Form送信
  • ルートに対応するactionがデータを受け取る
  • Remixがaction処理後に自動的に再検証を行う
  • これでユーザーからすればデータの作成とともにページの更新が行われる

といったことらしい。
HTMLとHTTPでの動作なので、JSを無効にしても動くとのこと。

疑問メモ

  • 1ページにおけるactionが複数になるケースはないものか?
  • root routeにactionを作成したが、actionはどの単位で持てるもの?

(なんか2つとも類似の疑問だが...)

改めてドキュメントを眺めつつ、
https://remix.run/docs/ja/main/route/action

A route action is a server only function to handle data mutations and other actions.

とあるので、route単位でactionは持てそうな気がする。
1ページで複数actionあったりする時どうするのって話は、多分呼び分ける方法があるのか、もしくはリクエスト情報から何らか分岐させるとかになるのかな。
ここではまだ想像の領域。

mm

Updating Data

https://remix.run/docs/ja/main/start/tutorial#updating-data

いきなり変わったファイル名をまた作ることになる。

ドキュメントとして参照した方が良いのは下記。
https://remix.run/docs/ja/main/file-conventions/routes#nested-urls-without-layout-nesting

例えば contacts.$contactId.tsxcontacts.$contactId.edit.tsxだと、パス的には下記のようになる

  • contacts.$contactId.tsx: /contacts/{contactId}
  • contacts.$contactId.edit.tsx: /contacts/{contactId}/edit

後者のパスに関しては一番近いパスとなる前者のパスにネストされたルートになって、
レイアウトのネストが発生することになる。

このレイアウトのネストを起こしたくない場合に、直前の(?)パスにおいてアンダースコア_をつけるとネストを回避できるというもの。

この場合は app/root.tsxにネストされるらしい。

ここのChapterでは更新用のルートとフォームの作成のみとなっており、実際の更新処理は次みたい。

mm

Mutation Discussion

https://remix.run/docs/ja/main/start/tutorial#mutation-discussion

どういう仕組みでフォームデータが送信されているか・処理しているのかの説明。

action関数とredirect関数を除けば、formDataなどは通常のWebとしての機能で、Remixが特別に提供しているものではないよという説明がある。

普段のformの取り扱いはライブラリ依存なこともしばしばあるが、こういう点では個人的にも好感が持てる。
(いわゆるWeb技術に直接触れることができている感じで、何しているかよくわからんが...という気持ちは少し減る)

===
ここまででデータの取得、作成や更新などを見てきたが、loader, actionが基本的に把握できていれば取り回しは非常にやりやすそうな気がする。
シンプルだなとも感じる。

mm

Redirecting new records to the edit page

https://remix.run/docs/ja/main/start/tutorial#redirecting-new-records-to-the-edit-page

リダイレクトを行う方法がわかったので〜というところで、Creating Contactsのchapterで準備したレコード作成用のactionにリダイレクト処理を作成して、即座に情報の編集画面を開くようにしましょうというもの。

redirect関数を上記処理に入れ込んでいくだけ。

mm

Active Link Styling

https://remix.run/docs/ja/main/start/tutorial#active-link-styling

サイドバーに表示しているcontactsから、今ページでアクティブなものをスタイリングしようよというもの。

remixが提供しているNavLinkコンポーネントを扱うことで実装できるよというもの。
(ここまでのチュートリアルではLinkコンポーネントを使っていたが、それを置き換えるイメージ)

Linkではダメで、NavLinkだとこういうことができるのは、
NavLinkの場合はtoにマッチするルートである場合はアクティブかどうかを判断してくれて、
かつclassName にコールバック関数を渡すことができて、その中でアクティブ判定を元にスタイルを決めることができるようになっているためである。
https://remix.run/docs/en/main/components/nav-link

Linkでも勿論個別に関数を書けば実現できるだろうが、ビルトインでこういうコンポーネントがあるなら活用する形で良さそう。

mm

Global Pending UI

https://remix.run/docs/ja/main/start/tutorial#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みたいなことだろうな。(実装の仕方は全く異なるが)

mm

Deleting Records

https://remix.run/docs/ja/main/start/tutorial#deleting-records

まだやってなかったレコードの削除処理の実装。

この時点で、削除ボタンはあるもののその先の処理は未実装なのでそれをどう処理するかという状態。

すでにapp/routes/contacts.$contactId.tsxで用意されている削除ボタンのFormでは
action="destroy"と指定がある。この場合は相対パスでdestroyのルートに対して処理が行われるので、

  • 新ルートとして、相対パスである app/routes/contacts.$contactId.destroy.tsx を作成
  • そのルートで削除のためのaction関数を用意する必要がある
  • この時、actionで即座にredirectを行う

のようなことを実施する必要がある。

mm

Index Routes

https://remix.run/docs/ja/main/start/tutorial#index-routes

ルートページの場合、Outletである箇所には何も一致する子コンポーネントがないので、空白がある

これをデフォルト表示で埋めたいよね!という話(?)

app/routes/_index.tsxを作成して、そこでコンポーネントを定義する。

_index.tsxは特別なファイルで、親ルートと一致するパスにいる場合に表示されるコンポーネントであって、この時Outletに表示するコンポーネントはないもの、という理解で良さそう (正直うまく説明できない感じ)

mm

URLSearchParams and GET Submissions

https://remix.run/docs/ja/main/start/tutorial#urlsearchparams-and-get-submissions

ここまでのフォーム送信はいずれもPOSTによるaction関数への送信処理のみだったが、
ここにきてGETによるフォーム送信 (この時の送信情報はURLSearchParamsというのは結構前のchapterで説明があった気がする)。

GETのフォーム送信ではPOSTのようにaction関数と紐づいているみたいなイメージよりは、
通常のGETリクエスト(e.g. リンクの遷移等) と同じ扱いになるので、
loader関数での処理になる。

mm

Synchronizing URLs to Form State

https://remix.run/docs/ja/main/start/tutorial#synchronizing-urls-to-form-state

GETフォームでは検索ボックスでの処理を想定した実装を行なったが、これだけでは不十分で、
例えばヒストリーバックした場合にテキストボックスに入力文字列の状態が残っていたり、
逆にリロードすると結果はフィルタされていても、テキストボックスには何も値がなかったり、
URLの状態と一致していない画面の状態となっている。

とはいえここでやっていることとしては、loaderで、検索結果の情報に加えてクエリの情報を返すのみで、
それをテキストボックスのデフォルト値に設定しているというもの。
これは通常のUX体験としての実装なので、そこまでRemixによった実装というものでもないかも。
(それでもloader使うじゃんというのはあるが、感覚的な問題ですかねえ。。)

ただ、これだけでは下記の同期はできないので、ここで useEffectを使ってDOMに対しての値設定で同期を行っている。

ヒストリーバックした場合にテキストボックスに入力文字列の状態が残っていたり

mm

Adding Search Spinner

https://remix.run/docs/ja/main/start/tutorial#adding-search-spinner

検索フォームなどの場合に、検索中であることをスピナーコンポーネントで知らせてあげようねというもの。

ここではuseNavigateフックを使って、navigationの状態からスピナーの表示・非表示を制御している。

こんな条件を追加した。

const searching =
    navigation.location &&
    new URLSearchParams(navigation.location.search).has(
      "q"
    );

navigation.location.searchというのがいきなり出てくるけどなんでしょうな。
https://remix.run/docs/ja/main/hooks/use-navigation#navigationlocation

In the case of a GET form submission, formData will be empty and the data will be reflected in navigation.location.search.

https://developer.mozilla.org/ja/docs/Web/API/Location/search

search は Location インターフェイスのプロパティで、クエリー文字列とも呼ばれる検索文字列です。つまり、 '?' の後の URL 引数を含む文字列を指定します。

要はクエリパラメータのことか。

なので追加した条件は「ナビゲーションが発生していて、かつqのクエリでの検索が発生している」という条件になるのかな。

mm

Managing the History Stack

https://remix.run/docs/ja/main/start/tutorial#managing-the-history-stack

さっき実装したFormでのonChangeハンドラでのsubmitに関連。
これによって都度キーストロークごとに送信が行われるので、膨大な履歴が溜まっちゃうよ!というのを回避する実装をしたいというもの。

ここでは、「テキストボックスの入力が空である場合は初めての入力として、初めての入力でない(=すでに入力値が存在する場合) は、履歴をpushするのではなく、現在のエントリと置き換える」ということをしている。

useSubmitのreplaceオプションが参考になる。
https://remix.run/docs/en/main/hooks/use-submit#signature

replace: Replaces the current entry in the history stack, instead of pushing the new entry. Default is false.

mm

Forms Without Navigation

https://remix.run/docs/ja/main/start/tutorial#forms-without-navigation

ナビゲーションを発生させずにフォーム送信を行いたいというニーズに対応する機能。
同一ページ内でのデータ変更である場合に利用することになりそう。

useFetcherフックがこれを実現する。
https://remix.run/docs/ja/main/hooks/use-fetcher

useFetcherを呼び出して、<fetcher.Form ...></fetcher.Form>のようにするだけでいいらしい。

データの再検証等も行なってくれるので、本当にナビゲーションのみを除外したフォームということになりそうだ。

mm

Optimistic UI

https://remix.run/docs/ja/main/start/tutorial#optimistic-ui

サンプルアプリケーションでは実際のデータ処理も考慮して、処理時にダミー的に処理に時間がかかるようにしてある。
先ほど実装したナビゲーションなしのフォーム処理では、⭐️をつけるか否かの処理だった。

この時点では、フォーム送信後に状態が反映されるまで時間がかかったが、楽観的UI (optimistic UI)として、みなし的に画面の状態を更新するようにしても良いのではないかという提案である。

ここでは、
「fetcherが持つformDataの状態を確認できる場合(=データ送信処理後)は、その状態を元にコンポーネントを再レンダリングさせて、そうでない場合は実際の状態に合わせてコンポーネントをレンダリングする」ということを行なっている。

mm

これでtutorialとしては以上。

Discussion Topics も面白そうというか、おそらく開発を進める上では目を通しておいた方が良さそうなので、これは順次読み進めていく。

mm

Tutorialとしてのスクラップだったので、こちらはクローズする

このスクラップは4ヶ月前にクローズされました