RemixでNested Route構成のLoaderFunctionを考えてみる
RemixでNested Routeを構成している場合に、親ルートのLoaderFunctionで取得した値を子ルートで活用できるのか(子で省略できるのか)を考えてみました。
選択肢
子で記載する以外は以下の2択が基本かもしれません。
useOutletContextを利用してOutletのPropsとして渡す
子のloader内で親のloaderの結果を取得する(呼び出す)
しばらくuseOutletContextを試していたのですが、子にloaderを書きにくい(少し混乱しそう)なので、ここでは親のloaderの結果を取得する方法を見てみました。
構成(と結論)
Nested Routeの構成は抜粋すると以下のようになります。
routes/
user.tsx
user.detail.tsx
(親)"user.tsx"のloader部分はこのようになります。
export const loader: LoaderFunction = async ({ request }) => {
const { user, session } = await getUser(request);
const headers = { "Set-Cookie": await commitSession(session) };
return json({ user }, { headers });
};
export default function User() {
return <Outlet />;
}
(子)"user.detail.tsx"のloader部分はこのようになります。
import { loader as parentLoader } from "./user";
export const loader: LoaderFunction = async ({ request, params, context }) => {
const parentResponse = await parentLoader({ request, context, params });
const parentJson = await parentResponse.json();
const data = { ...parentJson, customValue: "abc" };
};
export default function UserShow() {
const data = useLoaderData<typeof loader>();
console.log(data);
return (
<div>{data.user.name}</div>
...
}
これで、子の useLoaderData では親のloaderで取得したデータと子のloaderで取得したものを合わせて取得できます。
上記コードでは、子のloaderの独自データは customValue となっています。
ポイント
子のloaderで親のloaderを呼び出す際にはrequest, params, context全てを指定する必要がある
parentLoader の引数の型は DataFunctionArgs となっており、remix-server-runtimeを確認すると以下でした。
export interface DataFunctionArgs {
request: Request;
context: AppLoadContext;
params: Params;
}
export interface LoaderFunction {
(args: DataFunctionArgs):
| Promise<Response>
| Response
| Promise<AppData>
| AppData;
}
親のloaderのreturnはResponse型になっている
単純にfetchと同じという印象です。というかRemixのドキュメントにもそのような感じに記載されてます。
なので、子のloaderで親のloaderを呼び出すというのも、fetchを行っているだけと見ればよさそうです(なのでjsonの扱いができる)
親のloaderで用意したheadersは子のResponseに含まれる
子のloaderのreturnにはheadersを含めていませんが、親のloaderで設定したheadersが渡されていることをChromeのDevToolsで確認できます。
これもRemixの仕組みだとは思うのですが、今回は確認できていません。
あと、loaderの return json(…) が気になったので少しソースコードを追いかけてみました。
少し入り組んでました。
@remix-run/node
↓
remix-run/remix
↓
packages/remix-node
↓
packages/remix-server-runtime
↓
packages/remix-server-runtime/responses.ts
import {
defer as routerDefer,
json as routerJson,
redirect as routerRedirect,
type UNSAFE_DeferredData as DeferredData,
type TrackedPromise,
} from "@remix-run/router";
/**
* This is a shortcut for creating `application/json` responses. Converts `data`
* to JSON and sets the `Content-Type` header.
*
* @see https://remix.run/utils/json
*/
export const json: JsonFunction = (data, init = {}) => {
return routerJson(data, init);
};
(違うリポジトリいったよ)
remix-run/react-router
↓
packages/router
↓
packages/router/utils.ts
export type JsonFunction = <Data>(
data: Data,
init?: number | ResponseInit
) => Response;
/**
* This is a shortcut for creating `application/json` responses. Converts `data`
* to JSON and sets the `Content-Type` header.
*/
export const json: JsonFunction = (data, init = {}) => {
let responseInit = typeof init === "number" ? { status: init } : init;
let headers = new Headers(responseInit.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/json; charset=utf-8");
}
return new Response(JSON.stringify(data), {
...responseInit,
headers,
});
};
なるほど、確かに結局Responseを変えてますね。