Remixについて調べる
Routingのルール
RemixはNested Routingを採用している。Nested RoutingはRemixの肝なので最初に理解しておくべき(ここを理解してないと何もわからんと思う)。Nested Routingとは、URLのセグメント(/hoge/bar/fooでいうところのhogeとかbar)をUIのコンポーネント階層に結合させるという一般的な考え方。どういうことかというのは公式ドキュメントにある下記の図を見てもらうのが早い。URLとページ内の各コンポーネントのレイアウトがそれぞれ対応して1つの画面を作っている。
https://scrapbox.io/files/649bd885ed224b001bfcd600.gif
code:tsx
<Root>
<Sales>
<Invoices>
<InvoiceId />
</Invoices>
</Sales>
</Root>
基本ルールはファイルベースルーティングであり、.がURLの/になる。sales.invoices.$invoiceId.tsx => /sales/invoices/:invoiceIdと対応している。これらはapp/routes/*に下記のようにファイルを作っていけば良い。
code:sh
app
├── root.tsx
└── routes
├── _index.tsx
├── sales._index.tsx
├── sales.invoices.$invoiceId.tsx
├── sales.invoices.tsx
└── sales.tsx
Routeモジュール(以下Route)という概念がある。Routeは普通のJavascriptのmoduleであり、URLのセグメントに紐づく。
RouteモジュールはURLのセグメントのみにマッピングされるため、1つのURLで複数のルートをレンダリングすることができる。コンポーネント階層はURLセグメント階層にマッピングされる。
例えば/sales/invoices/:idというページはsalesとinvoicesとinvoice.$idのRouteが組み合わさって出来ている。Routeのファイルとしては、下記のようなものをそれぞれ作ることになる。
root.tsx
routes/sales.tsx
routes/sales.invoices.tsx
routes/sales.invoices.$invoiceId.tsx
<Outlet />というRouteを使うと親のRouteを維持したまま別のRouteを<Outlet />部分に適用できる。例えばサイドバーはRoot RouteでレンダリングされているがメインコンテンツはArticle Routeで管理したい場合とかにはRoot Routeに<Outlet />を記述しておけばArticle Routeがその部分に適用される。
ちなみにTutorialを見てるとposts.tsxとposts._index.tsxというのが定義されてるがどちらもIndex Routeだけどなんで二つあるのか?これはposts.tsxの方にそのIndex RouteのRoot Routeのような役割をさせるため。ユースケースとしては/posts/hoge/barなどの/posts配下の全てのページのみで出したい共通のヘッダーメニューがある場合などにはposts.tsxへヘッダーメニューを定義することで/posts配下の全ページへ共通のメニューが出せる。posts._index.tsxの方は/postsでのみ表示されるものを定義する。
/posts/$slug/editはどう作る?一見posts.$slug.edit.tsxを作れば良いように思えるがこれだと微妙。なぜならposts.$slugに対してeditがネストされてしまう(/posts/admin/$slugで記事ページのみ表示したいのにeditフォームが混在して表示されるなどが発生する)。この場合はtrailing_アンダースコアを使うとよい。posts.$slug_.edit.tsxのように定義するとEdit Routeはposts.$slugにネストされずPost Route(posts.)の直下にレンダリングすることができる。
trailing_アンダースコアと似たような機能として_leadingアンダースコアというのもある。これは例えばレイアウトとしてはauth.logingという風にしたいがURLとしては/auth/loginではなく/loginになってほしい場合。この場合にファイルパスを_auth.login.tsxとleadingアンダースコアを使うと/loginというURLにできる。
ドキュメントにもある通り、下記の文章が端的に違いを表している。
_leading underscore opts-out of URL nesting
trailing_ underscore opts-out of layout nesting
hoge.$.tsxみたいな$だけを使ったRouteを定義できる。$の部分はいわゆるワイルドカードマッチになる。これをSplatsという。loaderやaction内でのparamsからはparams[*]としてアクセスできる。hoge.$.tsxであれば/hoge/a/b/c/d/e.jpgというアクセスに対してparams[*]はa/b/c/d/e.jpgと返す。
Optimistic UI
Optimistic UIとはユーザーのインタラクションに対してバックエンドの事情を無視して即座にフロントエンドに反映させる仕組みのこと。例えばTwitterでツイートにいいね!をすると裏側でいいね!がDBに反映するのを待つことなくUI上では即座に赤いハートになる。
これにはuseNavigation | Remixを使ってnavigation.formDataという感じでForm入力した情報をキャッチし、その情報を元に先にviewだけレンダリングさせる。バックエンドへの通信が終わったら本来のviewページへリダイレクトさせる。それだけで基本的にはOK。 クライアント側のコードとサーバー側のコードの境界
こういうフロントエンドフレームワークを使う時に不安になるのが書いたコードがフロントエンドで実行されるのかサーバー側で実行されるのかわからないこと。例えばサーバー側で実行されるからと秘匿情報を扱うコードを書いていたら謝ってクライアント側に漏れ出してたみたいなケース。この辺を理解しておかないと後々事故る。
RemixではまずRouteコンポーネントで定義されるloaderやactionはサーバー側で実行される。またhoge.server.tsという感じで.server付きで定義されたファイルもサーバー側で実行される。 サーバー側で実行されるコードにはwindowやdocumentなどのDOMへのアクセスがされないように気を付ける。逆にクライアント側のコードにprismaのコードがバンドルされないように気を付ける。
認証
indie stackのテンプレを用いて作成したTutorialではデフォルトでemailとパスワードのログインが実装されている。ユーザー情報はsqliteに格納しSessionはクッキーで管理されている。user情報はRootのRouteに格納されており、useMatches | Remixを使ってRoutesへアクセスし取得できる。 RootのRouteなど認証でアクセス制限したい場所をIndex Routeで認証を挟めば基本的には大丈夫そう。認証制限配下のRouteの一部だけはPublicアクセス可能にしたい場合はtrailing underscoreを設定すれば一応回避できるだろう(共通要素も再実装になるから冗長になりそうだけど)。
Third Party認証
これを見た感じだとやはりclientサイドで色々やる場合はuseEffectで頑張るだけっぽい。
バッチ処理
なんかだり〜〜〜
クエリパラメータで処理分岐
Third Party認証のところと同じでuseEffectで頑張るだけ
雑多なメモ
loader,actionを通じたシームレスなサーバーとのやりとりは確かにうまい
routingとrendering周りの規則の理解は最初つまづきそう
reactでいうところのuseEffectはどうやれば良いんだろう
普通にreactの標準のやつをimportすれば良いだけだった。
Reactに標準定義されているuseTransition – Reactとは違うuseTransitionがRemixにも定義されていたがこれはdeprecatedになっている。今はuseNavigationを使うべきらしい。 現在いるRouteまでのRoute情報を全て返してくれるやつ。Docsではパンクズリストを作る時とかに便利とか書いてあった。tutorialではRootのRouteにログイン情報を保持するようにしているが、もし子Routeからログイン情報にアクセスしたい場合などにuseMatchesでRootのRouteへアクセスしuser情報にアクセスするみたいな感じで利用されていた。
entry.server.tsxというファイルは何か?
これはRemixがサーバー側で生成したHTTPのレスポンスを管理する場所。etagをレスポンスヘッダーに付与したいとかそういうカスタマイズもここでやると全HTTPレスポンスに反映できて便利。
loaderの抽象化のために丸っと別関数に切り出すとRemixのコンパイラがcomponentとloaderを切り分けられないのでbad practiceらしい。
editフォームでloaderから取得したpropsが変わっても反映されないという事象が発生。
原因はpropsが変わっても<Form />が再レンダリングされないということ。なぜ再レンダリングされなかったかというとkeyを設定してなかったので。key={post?.slug ?? "new"}みたいにslugが変わったら<Form />が再レンダリングされるようにしたら意図通りに動いた。初歩的だ...
リソース
公式Docs
ブログutorial
雰囲気解説 in Zenn