Closed5

RemixでLoaderFunctionからloaderHeadersにデータが渡らない

Hi MORISHIGEHi MORISHIGE

RemixでmicroCMSのプレビュー機能を利用するため、draftKeyが付与された時にheaderを動的に切り替えたい。loaderHeaders.get('Cache-Control')には必ずnullが入ってしまう。

app/routes/posts/$postId.tsx
// dataLoaderからheaderが渡された場合に切り替える。デフォルトはstale-while-revalidate
export const headers: HeadersFunction = ({ loaderHeaders }) => {
  const cacheControl =
    loaderHeaders.get('Cache-Control') ??
    'max-age=0, s-maxage=60, stale-while-revalidate=60';
  return {
    'Cache-Control': cacheControl,
  };
};

// microCMS APIから記事詳細を取得する
export const loader: LoaderFunction = async ({ params, request }) => {
  // 下書きの場合
  const url = new URL(request.url);
  const draftKey = url.searchParams.get('draftKey');

  // 記事を取得

  // 下書きの場合キャッシュヘッダを変更
  const headers = draftKey
    ? { 'Cache-Control': 'no-store, max-age=0' }
    : undefined;

  return json(content, { headers });
};
Hi MORISHIGEHi MORISHIGE

暫定の解決策

app/root.tsxLoaderFunctionを追加。

親となるrootのloaderに何かしらデータがあるもしくは明示的にnullであれば処理がうまくいっている模様。

app/root.tsx
export const loader: LoaderFunction = async () => {
  return null;
};
Hi MORISHIGEHi MORISHIGE

暫定の解決策に至るまでの経緯

探ってみたところ同じ現象はすでに下記issueで報告されていた。
https://github.com/remix-run/remix/issues/1140

データの流れは下記のような感じ
dataLoader -> middleware(json helper) -> headerLoader

指摘箇所はjson helperだったが、helper functionを書き換えるのは他にも影響あるかと思いdataLoader側から該当箇所を出力する部分を調整するPRを投げた。

https://github.com/remix-run/remix/pull/1705

開発者のryanflorence氏からフィードバックをもらうが、ryanflorence氏の環境では該当の問題は起きていないとissueをクローズしてしまった。

いやいや、ということで、症状を再現するテストコードとcodesandbox環境を用意した。

https://codesandbox.io/s/intelligent-sammet-losgp?file=/app/routes/index.tsx

改めてryanflorence氏からフィードバック。

Ah we have regression here, at the moment, headers incorrectly requires that every parent route has a loader. We'll fix this soon, thanks for your work on this :)

どうやら、rootのloaderからもloaderが渡ってくる前提でheaderLoaderのロジックが組まれてしまっていることが原因らしい。

ということでissueは再オープン。

Hi MORISHIGEHi MORISHIGE

さらに調べた感じ下記getDocumentHeadersで問題が発生する。
ここに渡す前に正しくデータを用意できればよいのかもしれない。
引続き調査。

function getDocumentHeaders(build, matches, routeLoaderResponses, actionResponse) {
  // matches is [root, child], routeLoaderResponses is [child]
  return matches.reduce((parentHeaders, match, index) => {
    // index 0, routeModule is root
    let routeModule = build.routes[match.route.id].module;
    // index 0, response is from child (since I don't have a root loader)
    let loaderHeaders = routeLoaderResponses[index] ? routeLoaderResponses[index].headers : new Headers();
    let actionHeaders = actionResponse ? actionResponse.headers : new Headers();
    // since root doesn't export headers, i end up with undefined and lose my headers :(
    let headers = new Headers(routeModule.headers ? typeof routeModule.headers === "function" ? routeModule.headers({
      loaderHeaders,
      parentHeaders,
      actionHeaders
    }) : routeModule.headers : undefined);
    // Automatically preserve Set-Cookie headers that were set either by the
    // loader or by a parent route.
    prependCookies(actionHeaders, headers);
    prependCookies(loaderHeaders, headers);
    prependCookies(parentHeaders, headers);
    return headers;
  }, new Headers());
}
このスクラップは2022/02/11にクローズされました