見出し画像

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のドキュメントにもそのような感じに記載されてます。

On navigations in the browser, Remix will call the function via fetch from the browser.

loader | Remix

なので、子のloaderで親のloaderを呼び出すというのも、fetchを行っているだけと見ればよさそうです(なのでjsonの扱いができる)

親のloaderで用意したheadersは子のResponseに含まれる

子のloaderのreturnにはheadersを含めていませんが、親のloaderで設定したheadersが渡されていることをChromeのDevToolsで確認できます。
これもRemixの仕組みだとは思うのですが、今回は確認できていません。

あと、loaderの return json(…) が気になったので少しソースコードを追いかけてみました。

Using the json helper simplifies this, so you don't have to construct them yourself

Returning Response Instances

少し入り組んでました。
@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を変えてますね。


いいなと思ったら応援しよう!