結論
Remixはいいぞ!!!
サーバー側コードとクライアント側コードの切り分けは脳内で行わなければならなりませんが、裏を返せば「真の意味でのフルスタックフレームワーク」だといえます。
作ったもの
じゃんけんのタイマンが楽しめます
動機
- DiscordでVCしていたらじゃんけんする話になったが、音声だけだと難しかった
- 勝敗にを記録し、公式性を持たせたい
2日で作って3日で飽きられました
学んだこと
- ルーティング
- パスパラ取得
- サーバー側関数
- useFetcher
↑多分これだけ抑えれば動くものは作れます
やりたかったけど手が回ってない(そのうちやるかも?)
- クエリパラメータ(多分簡単)
- ロード状態監視・楽観的更新
- 無限スクロール系(どちらかというとdbクライアントを使いこなす話になる?)
ちょっと詰まったこと
- クライアント側環境変数の取り回し(firebaseConfigの渡し方)
- useEffect内でのuseFetcher使用で無限リロード問題
Nextと比べてどうなの?
書きやすいです!
仮想ルーターとしてしかReact Routerを使ったことない人は是非試してほしいです!
SSGするならNext
それ以外はRemix
バックエンドがいらなければviteの使い分けがいいです。
Remix混乱したポイント
Outletに何が挿入されるか問題
Outletは子ルートのコンポーネントをレンダリングしますが、Outletとだけ書かれるので宣言的ではないと感じます。
NextでいうLayout?でもLayoutもroot.tsxにあるので使う風習はありそうです。
import { Outlet } from "react-router";
export default function SomeParent() {
// ファイル名を確認しないと何がOutletに挿入されるかわからない
return (
<div>
<h1>Parent Content</h1>
<Outlet />
</div>
);
}
同じファイルなのにクライアント側コードとサーバー側コードがある
始めたては違和感がありますが、クライアント側で作成した変数をサーバー側で参照するとコンソールにエラーが出るので助かります。
↓この基本形で大抵のことはできます。
import { LoaderFunction } from "react-router";
import { useLoaderData } from "react-router";
import { useFetcher } from "react-router";
export const loader: LoaderFunction = async ({ request, params }) => {
// サーバー側
// GETの処理 外部からFetchされてもリロードされない
const fooId = params.fooId as string; // foo.$fooId.tsxなのでパスパラメータがある
return json({});
};
export const action: ActionFunction = async ({ request, params }) => {
// サーバー側
// GET以外のメソッドを処理。リソース変更が考えられるのでFetchするとリロードされる(mutationが簡単)
return json({})
};
export default function Home() {
// クライアント側
const loaderData = useLoaderData();
const fetcher = useFetcher(); // 好きなパスにリクエストするときに使う。
return (
<div>
</div>
);
}
僕は
- ファイル数削減
- import文削減
- 取得、利用までの見通しが良い
の3点で今は気に入っています。
ありそうな質問
インフラは何がいい?
ほぼすべてのJavaScriptランタイムで動くので場所は選ばないです。
cloudflareは公式対応あって評判もよさそうです。
SSRをする都合上、サーバーへの負荷が高めです。スケーリングは前提にしましょう。
今回はfirebaaseを使用した都合でGCP Cloud Runにしました。
SSRは時代遅れ?そんなぁ...
詰まったことの解決法
環境変数の渡し方
バックエンドから渡します。
root.tsx内にもloaderを設置できます。
+ export const loader: LoaderFunction = async () => {
+ return json(JSON.parse(process.env.FIREBASE_CONFIG ?? ""));
+ };
export function Layout({ children }: { children: React.ReactNode }) {
const config = useLoaderData();
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
useEffect内でのuseFetcher使用で無限リロード問題
ニッチな情報かもしれません。
useFetcherとuseEffectは食い合わせが悪いです。
例えば本アプリではfirebaseドキュメントの監視を行い、変更があった場合にプレイヤーの出した手をfetchする処理があります。
監視処理の開始はuseEffectで行い、ドキュメント変更時のコールバック関数内でfetchを使用する方法が基本かと思われます。
ここで、依存配列にfetcher自体を含めるべきであるという警告が出るのですが、fetcherの状態がfetchによって書き換わるため無限に再レンダリングされます。
やむを得ずeslintを無効にしました。
参考
// subscribe room, host user, guest user
useEffect(() => {
const db = getFirestore();
const unsub = onSnapshot(doc(db, "rooms", roomId), async (rd) => {
// 変更後のデータrdを処理する。省略
...
// お互いの手を取得する(この呼び出し自体がfetcherの状態を変えるので、依存配列にfetcherを入れてはならない)
hostHandFetcher.load(
`/rooms/${roomId}/users/${room.hostUser}/hands/${room.hostUserRound}`
);
guestHandFetcher.load(
`/rooms/${roomId}/users/${room.guestUser}/hands/${room.guestUserRound}`
);
});
return () => {
unsub();
};
// fetcherを依存配列に入れない
// https://github.com/remix-run/remix/discussions/3657
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [roomId]);
↑pathはFireStoreの構造と一致させることで混乱なく設計を進められました。
/
├── room
│ ├── :roomId
│ └── users
│ ├── :userId
│ └── hands
│ └── :handsId
├── users