type holyshared = Engineer<mixed>

技術的なことなど色々

React Router v8のミドルウェア

React Router v7からv8に対応させる作業をやっていた。 一部のものだけログイン画面でログインしようとして、リダイレクトを繰り返す様になってしまったので調査と修正をした。

ログイン画面があるものは、v8で有効になるミドルウェアをfutureフラグで対応していて、これを消すだけくらいかなと思っていました。

// react-router.config.ts
export default {
  appDirectory: "src/app",
  ssr: true,
  buildDirectory: "dist"
  buildDirectory: "dist",
  // このフラグを消す
  future: {
    v8_middleware: true,
  },
} satisfies Config;

しかし、ローカルで試すとログイン前にミドルウェアで認証状態の確認を行っている箇所に引っかかってログイン画面にリダイレクトする処理が何度も実行される様になってしまいました。

原因はsingle fetchの挙動が変わったことが原因の様でした。 url.pathnameの末尾に.dataが付くパターンがあり(具体的にはaction)、そのせいで未認証のチェックに引っかかり、無限にリダイレクトするということが起きていました。

これはパスの末尾に.dataがついていることも考慮に入れることで解決しました。 ミドルウェアがloader, actionで実行されることを意識すれば解決しそうな話でした。

import { redirect } from "react-router";
import { getSession, destroySession } from "~/session";
import { type RouterContextProvider } from "react-router";
import { userContext } from "~/context";

export const authMiddleware = async ({
  request,
  context,
}: {
  request: Request;
  context: Readonly<RouterContextProvider>;
}) => {
  const url = new URL(request.url);
  const ctx = context.get(userContext);

  // React Router の single fetch ではデータリクエストのパスが `/sign_in.data`
  // のようになるため、`.data` を除去してから判定する
  const pathname = url.pathname.replace(/\.data$/, "");

  if (pathname === "/sign_in") {
    if (request.method === "GET" && ctx.user) {
      throw redirect("/");
    }
    return;
  }

  if (!ctx.user) {
    const session = await getSession(request.headers.get("Cookie"));
    throw redirect("/sign_in", {
      headers: {
        "Set-Cookie": await destroySession(session),
      },
    });
  }
};