#`cacheComponents: true` is incorrectly requiring Suspense boundaries for `useSearchParams()`

1 messages · Page 1 of 1 (latest)

stuck trench
#

16.0.1 with cacheComponents: true is incorrectly requiring Suspense boundaries for useSearchParams() in a fully dynamic route that should not be prerendered. Attempted to wrap the component with useSearchParams() with Suspense boundaries at multiple levels (layout, page, and component), yet the build still fails with:

⨯ Render in Browser should be wrapped in a suspense boundary at page "/auth/partner-signin"
sage wyvernBOT
#

🔎 This post has been indexed in our web forum and will be seen by search engines so other users can find it outside Discord

🕵️ Your user profile is private by default and won't be visible to users outside Discord, if you want to be visible in the web forum you can add the "Public Profile" role in id:customize

✅ You can mark a message as the answer for your post with Right click -> Apps -> Mark Solution
(if you don't see the option, try refreshing Discord with Ctrl + R)

stuck trench
#

Attempting to make a clean reproduction but haven't managed to reproduce yet. My subject repo is complex.

carmine saddle
stuck trench
#

@carmine saddle Thanks. The error states the route the error occurs at. /auth/partner-signin

#
// /auth/partner-signin/page.tsx

import { Suspense } from "react";
import PartnerSignIn from "./PartnerSignIn";

export default function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <PartnerSignIn />
    </Suspense>
  );
}
#
"use client";

import { Suspense, useEffect, useState } from "react";
import { signIn } from "next-auth/react";
import { useSearchParams, useRouter } from "next/navigation";

export default function PartnerSignIn() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const [status, setStatus] = useState<"loading" | "signing-in" | "error">(
    "loading",
  );

  const token = searchParams.get("token");
  const email = searchParams.get("email");

  useEffect(() => {
    if (!token || !email) {
      queueMicrotask(() => {
        setStatus("error");
      });
      router.push("/login?error=invalid-partner-link");
      return;
    }

    fetch("/api/auth/verify-partner-token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ token, email }),
    })
      .then((response) => response.json())
      .then(async (data) => {
        if (data.valid) {
          queueMicrotask(() => {
            setStatus("signing-in");
          });
          // Sign in using email provider with pre-verified email
          const result = await signIn("email", {
            email: email,
            redirect: false,
            callbackUrl: data.redirectUrl || "/app?welcome=partner",
          });

          if (result?.url) {
            // Instead of email verification, go directly to the app
            router.push(data.redirectUrl || "/app?welcome=partner");
          } else {
            queueMicrotask(() => {
              setStatus("error");
            });
            router.push("/login?error=signin-failed");
          }
        } else {
          queueMicrotask(() => {
            setStatus("error");
          });
          router.push("/login?error=invalid-token");
        }
      })
      .catch(() => {
        queueMicrotask(() => {
          setStatus("error");
        });
        router.push("/login?error=system-error");
      });
  }, [token, email, router]);

  return ... ;
}
carmine saddle
#

what about:
/auth/partner-signin/layout.jsx
and
/auth/layout.jsx
and
/layout.jsx

?

does any of them have a chance to have used useSearchParams?

stuck trench
#

/auth/layout.tsx

import { Suspense } from "react";

export default function AuthLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>;
}

#

Root layout

import "@/styles/globals.css";

type LayoutProps = {
  children: React.ReactNode;
};

export default async function RootLayout(props: LayoutProps) {
  const { children } = props;
  return children;
}
carmine saddle
stuck trench
#

There is no /auth/partner-signin/layout.tsx

carmine saddle
#

You're right. this is suspicious

#

try removing/commenting every other route EXCEPT /auth/partner-signin

stuck trench
#

oof.

#

I know something weird is going on because I tried to make a repropduction project with just that route and it works.

carmine saddle
#

exactly. its weird. maybe try it in the previous project

#

hmmm you know, maybe try removing the .next folder

stuck trench
#

ok tried removing all other routes. from CLAUDE

⏺ Excellent! This confirms it's NOT related to generateStaticParams in the [domain] routes.

  The error still occurs with only 4 pages (Generating static pages (0/4)) and just the /auth route active. Notice it now says "Generating static pages (0/4)" instead of "(0/2199)".

  This means:
  - :white_check_mark: The issue is isolated to the /auth/partner-signin route itself
  - :white_check_mark: NOT caused by other routes with generateStaticParams
  - :white_check_mark: The route structure with Suspense boundaries is still failing

  Let me check what those 4 pages are and look more carefully at the /auth route structure:

⏺ Bash(find /Users/alexnelson/Sites/ontempo/app/auth -type f -name "*.tsx" -o -name "*.ts" | sort)
  ⎿  /Users/alexnelson/Sites/ontempo/app/auth/layout.tsx                      
     /Users/alexnelson/Sites/ontempo/app/auth/partner-signin/page.tsx
     /Users/alexnelson/Sites/ontempo/app/auth/partner-signin/PartnerSignIn.tsx

⏺ Bash(ls -R /Users/alexnelson/Sites/ontempo/app/auth)
  ⎿  layout.tsx                   
     partner-signin
     … +3 lines (ctrl+o to expand)

⏺ So only 3 files. Let me look more carefully at the actual Suspense setup. Let me re-read the files to see the current state:

⏺ Read(app/auth/partner-signin/page.tsx)
  ⎿  Read 10 lines

⏺ Read(app/auth/layout.tsx)
  ⎿  Read 10 lines

⏺ Perfect. So we have:
  - Layout with Suspense :white_check_mark:
  - Page with Suspense :white_check_mark:
  - Client component with useSearchParams :white_check_mark:

  This is the exact minimal reproduction. The setup is correct, yet Next.js 16 with cacheComponents: true is still trying to prerender it and failing.
#

Still getting the error

#

removing .next

#

Oh I've been removing .next for debugging. This is my debug script.

rm -rf .next && pnpm next build --debug-prerender

#

I'm stumped!

#

OK it has something to do with proxy.tsx

#

I reproduced the error

#

calling getToken() in my middleware is causing the issue

import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";

export const config = {
  matcher: [
    "/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)",
  ],
};

export default async function proxy(req: NextRequest) {
  const url = req.nextUrl;

  // Skip processing for font files
  if (
    url.pathname.includes("/_next/static/fonts/") ||
    url.pathname.match(/\.(ttf|woff|woff2|eot|otf)$/)
  ) {
    return NextResponse.next();
  }

  const hostname = req.headers
    .get("host")!
    .replace(".localhost:3000", `.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`);

  const searchParams = req.nextUrl.searchParams.toString();
  const path = `${url.pathname}${searchParams ? `?${searchParams}` : ""}`;

  // Create base response
  let response: NextResponse;

  // Handle app pages
  if (hostname == `app.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`) {
    const session = await getToken({ req });

    if (!session && path !== "/login") {
      const loginUrl = new URL("/login", req.url);
      response = NextResponse.redirect(loginUrl);
    } else if (session && path === "/login") {
      const homeUrl = new URL("/", req.url);
      response = NextResponse.redirect(homeUrl);
    } else {
      const appUrl = new URL(`/app${path === "/" ? "" : path}`, req.url);
      response = NextResponse.rewrite(appUrl);
    }
  }
  // Handle root application
  else if (
    hostname === "localhost:3000" ||
    hostname === process.env.NEXT_PUBLIC_ROOT_DOMAIN
  ) {
    const homeUrl = new URL(`/home${path === "/" ? "" : path}`, req.url);
    response = NextResponse.rewrite(homeUrl);
  } else {
    const customUrl = new URL(`/${hostname}${path}`, req.url);
    response = NextResponse.rewrite(customUrl);
  }

  return response;
}
#

@carmine saddle going to submit an issue. you agree?