next.js icon indicating copy to clipboard operation
next.js copied to clipboard

"Rendered more hooks than during previous render" when using App Router

Open mynameisankit opened this issue 1 year ago • 12 comments

Link to the code that reproduces this issue

Link

For more reproductions, see:

  • https://github.com/vercel/next.js/issues/63388
  • https://github.com/vercel/next.js/issues/78396
  • https://github.com/vercel/next.js/issues/80483

To Reproduce

  1. Start the application on port 3000
  2. Goto <base-url>/versions/v1
  3. Wait for a few seconds till the error screen is displayed
  4. Click on Goto Version v2

Current vs. Expected behavior

I expected to be redirected to <base-url>/versions/v2 but instead a client-side react error is triggered with the following stack trace

Uncaught Error: Rendered more hooks than during the previous render.
    at updateWorkInProgressHook (react-dom.development.js:11337:1)
    at updateMemo (react-dom.development.js:12470:1)
    at Object.useMemo (react-dom.development.js:13417:1)
    at useMemo (react.development.js:1777:1)
    at Router (app-router.js:215:58)
    at renderWithHooks (react-dom.development.js:11021:1)
    at updateFunctionComponent (react-dom.development.js:16184:1)
    at beginWork$1 (react-dom.development.js:18396:1)
    at HTMLUnknownElement.callCallback (react-dom.development.js:20498:1)
    at Object.invokeGuardedCallbackImpl (react-dom.development.js:20547:1)
    at invokeGuardedCallback (react-dom.development.js:20622:1)
    at beginWork (react-dom.development.js:26813:1)
    at performUnitOfWork (react-dom.development.js:25637:1)
    at workLoopSync (react-dom.development.js:25353:1)
    at renderRootSync (react-dom.development.js:25308:1)
    at recoverFromConcurrentError (react-dom.development.js:24525:1)
    at performConcurrentWorkOnRoot (react-dom.development.js:24470:1)
    at workLoop (scheduler.development.js:256:1)
    at flushWork (scheduler.development.js:225:1)
    at MessagePort.performWorkUntilDeadline (scheduler.development.js:534:1)

Provide environment information

Operating System:
  Platform: linux
  Arch: x64
  Version: #1 SMP PREEMPT_DYNAMIC Sun Aug  6 20:05:33 UTC 2023
Binaries:
  Node: 20.11.0
  npm: 10.2.4
  Yarn: 1.22.19
  pnpm: 8.15.1
Relevant Packages:
  next: 14.1.0
  eslint-config-next: 14.1.0
  react: 18.2.0
  react-dom: 18.2.0
  typescript: 5.3.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

App Router

Which stage(s) are affected? (Select all that apply)

next dev (local), next build (local), next start (local), Vercel (Deployed), Other (Deployed)

Additional context

I tested my reproduction on the following versions:-

  1. 13.4.19
  2. 14.1.0

mynameisankit avatar Mar 11 '24 07:03 mynameisankit

+1

sirajtahra avatar Jun 04 '24 15:06 sirajtahra

I have dig into this issue and found that call of useThenable inside of use hook doesn't seem to work properly. So, I tried change the implementation of useUnwrapState like this in my local machine, it worked.

function useUnwrapState(_state: ReducerState): AppRouterState {
  const [state, setState] = useState(_state);
  useEffect(()=>{
     if (isThenable(_state)) {
       _state.then(setState);
    } else {
      setState(_state)
    }
  }, [_state])

  return state
}

Maybe is it related to use hook and startTransition issue?

lifeisegg123 avatar Jun 07 '24 11:06 lifeisegg123

We're seeing this with a component that renders nothing and only redirects where another component is calling router.replace. Probably not the best code but still feels like we shouldn't see a varying amount of hooks rendering.

export default function OnboardingCheckComplete({
  onboardingStatus,
}: {
  onboardingStatus: OnboardingStatus
}) {
  const router = useRouter()
  const pathname = usePathname()

  useEffect(() => {
    if (
      pathname !== "/onboarding/welcome" &&
      onboardingStatus.onboarding.currentStep === OnboardingStatusComplete
    ) {
      router.replace("/onboarding/welcome")
      router.refresh()
    }
  }, [pathname, onboardingStatus.onboarding.currentStep, router])

}

And the page at /onboarding

export default async function OnboardingPage() {
   const status = await gateway.onboarding.status.get()

  if (status.onboarding.currentStep === OnboardingStatusNotStarted) {
    redirect("/onboarding/start")
  } else {
    throw new Error(
      `Invalid onboarding status ${status.onboarding.currentStep}`
    )
  }
}

jetaggart avatar Jul 19 '24 15:07 jetaggart

+1

Edit by maintainer bot: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote 👍 on the issue description or subscribe to the issue for updates. Thanks!

hunghuy201280 avatar Sep 19 '24 13:09 hunghuy201280

My error stack here

(anonymous) @ react-dom-client.production.js:11023
x @ scheduler.production.js:151
error-boundary-callbacks.ts:56 Error: Minified React error #310; visit https://react.dev/errors/310 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at lQ (react-dom-client.production.js:3494:1)
    at Object.aw [as useMemo] (react-dom-client.production.js:4161:1)
    at react.production.js:515:1
    at M (app-router.tsx:252:38)
    at lM (react-dom-client.production.js:3373:1)
    at oh (react-dom-client.production.js:6016:1)
    at o_ (react-dom-client.production.js:7012:1)
    at ic (react-dom-client.production.js:10829:1)
    at react-dom-client.production.js:10710:35
    at is (react-dom-client.production.js:10711:1)
    at u9 (react-dom-client.production.js:10289:1)
    at ij (react-dom-client.production.js:11606:1)
    at MessagePort.x (scheduler.production.js:151:1)
overrideMethod @ hook.js:608
(anonymous) @ console.js:36
s @ error-boundary-callbacks.ts:56
ot @ react-dom-client.production.js:5580
e.callback @ react-dom-client.production.js:5613
rC @ react-dom-client.production.js:2818
rz @ react-dom-client.production.js:2828
oO @ react-dom-client.production.js:7472
oK @ react-dom-client.production.js:7834
o6 @ react-dom-client.production.js:8678
oK @ react-dom-client.production.js:7802
o6 @ react-dom-client.production.js:8678
oK @ react-dom-client.production.js:7802
o6 @ react-dom-client.production.js:8678
oK @ react-dom-client.production.js:7802
o6 @ react-dom-client.production.js:8678
oK @ react-dom-client.production.js:7802
o6 @ react-dom-client.production.js:8678
oK @ react-dom-client.production.js:7868
o6 @ react-dom-client.production.js:8678
oK @ react-dom-client.production.js:7868
o6 @ react-dom-client.production.js:8678
oK @ react-dom-client.production.js:7838
iv @ react-dom-client.production.js:11190
ig @ react-dom-client.production.js:11050
u7 @ react-dom-client.production.js:10462
u9 @ react-dom-client.production.js:10387
ij @ react-dom-client.production.js:11606
x @ scheduler.production.js:151

himself65 avatar Jan 16 '25 07:01 himself65

/**
 * The global router that wraps the application components.
 */
function Router({
  actionQueue,
  assetPrefix,
}: {
  actionQueue: AppRouterActionQueue
  assetPrefix: string
}) {
  const [state, dispatch] = useReducer(actionQueue)
  const { canonicalUrl } = useUnwrapState(state)
  // Add memoized pathname/query for useSearchParams and usePathname.
  const { searchParams, pathname } = useMemo(() => { // <-- this line
    const url = new URL(
      canonicalUrl,
      typeof window === 'undefined' ? 'http://n' : window.location.href
    )

    return {
      // This is turned into a readonly class in `useSearchParams`
      searchParams: url.searchParams,
      pathname: hasBasePath(url.pathname)
        ? removeBasePath(url.pathname)
        : url.pathname,
    }
  }, [canonicalUrl])

himself65 avatar Jan 16 '25 07:01 himself65

@himself65 did you solve this?

sam-masscreations avatar Feb 05 '25 23:02 sam-masscreations

This issue happens to me when calling redirect in a page.tsx (RSC), it was fixed once I add the loading.tsx 🤷‍♂

maiconcarraro avatar Apr 10 '25 23:04 maiconcarraro

import type React from "react";
import { auth } from "@/server/auth";
import type { PropsWithChildren } from "react";
import { redirect } from "next/navigation";

export default async function AuthLayout({ children }: PropsWithChildren) {
  const session = await auth();
  if (session) { // ok
    redirect("/dashboard");
  }

  return <div className="min-h-screen">{children}</div>;
}

When redirect to the dashboard, an error occurs. I don't quite understand why. 🤔

stitched-error.ts:23 Uncaught Error: Rendered more hooks than during the previous render.

EndyZhou avatar May 16 '25 16:05 EndyZhou

import type React from "react"; import { auth } from "@/server/auth"; import type { PropsWithChildren } from "react"; import { redirect } from "next/navigation";

export default async function AuthLayout({ children }: PropsWithChildren) { const session = await auth(); if (session) { // ok redirect("/dashboard"); }

return <div className="min-h-screen">{children}; } When redirect to the dashboard, an error occurs. I don't quite understand why. 🤔

stitched-error.ts:23 Uncaught Error: Rendered more hooks than during the previous render.

"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

export function ClientRedirect({ url }: { url: string }) {
  const router = useRouter();
  useEffect(() => {
    router.replace(url);
  }, [url, router]);

  return null;
}

	...
  if (session) {
    return <ClientRedirect url='/dashboard' />
  }
  ...
}

No errors remain. Is redirection currently achievable only on the client-side?

EndyZhou avatar May 17 '25 03:05 EndyZhou

I think here the issue is that the people are trying to do a redirect in different places, with different tools, and considering everything is just a function.

Solution for each goal:

  1. Redirect from a server component
import { redirect } from 'next/navigation'

export default async function ProjectPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  if () {
  redirect(`${id}/overview`)
  }

  return null // or JSX
}

It's important to notice that this is a React component not a plain function. It MUST return null or JSX. Otherwise when it reaches the frontend (yes, even server components eventually land on the front), it will cause the error mentioned above. Sorry, hydration hurts a lot.

  1. Redirect from a server action
'use server'

import { redirect } from 'next/navigation'

export async function redirectToOverview(id: string) {
  // Your server action logic here
  redirect(`${id}/overview`) // No return needed
}

Here we can return or not whatever we want as it's essentially a plain function.

  1. Redirect from a client component

I also saw people doing something like this

  useEffect(() => {
    const performRedirect = async () => {
      const { id } = await params
      router.push(`${id}/overview`)
    }

    performRedirect()
  }, [params, router])

Here I don't even know how to start, because it gives me chills. Hopefully, nobody is doing this for real, but just in case please know that you can do this instead:

'use client'
 
import { redirect, usePathname } from 'next/navigation'
 
export function ClientRedirect() {
  const pathname = usePathname()
 
  if (pathname.startsWith('/admin') && !pathname.includes('/login')) {
    redirect('/admin/login')
  }
 
  return <div>Login Page</div>
}

Yes, redirect works on the client. And for those who are not using the latest versions of Next, here is the link to their doc, just browse to your version and you'll find a better way than a use effect to do a redirect: https://nextjs.org/docs/app/api-reference/functions/redirect#client-component

Hope this is helpful for someone. If I'm missing something let me know

P.S. I don't even want to explain why adding a suspense around a wrong-written redirect component works. I think "if it works, then it must be fine" is not cool anymore

ZGFuZHk100 avatar Jul 31 '25 15:07 ZGFuZHk100

Has anyone got any further solutions for this error beyond removing suspense boundaries/loading.tsx entirely? I'm surprised this hasn't been more widely reported to be honest.

I'm trying to improve UX by utilising suspense, but am blocked by this error repeatedly. I have tried using loading.tsx, opting for an inlined <Suspense>, removing not-found.tsx and <Link> inside them, and various other "solutions" mentioned in other threads etc, but have got no other fix beyond just straight up removing all instances of loading.tsx/<Suspense> entirely.

It's a bit frustrating as the dev mode UX (and the built version, when it's not throwing this error!) is fantastic, and Suspense seems amazing, but I am completely blocked in being able to use it anywhere on production at all.

I'm currently seeming to run into the error when a page renders a notFound()/not-found.tsx (either Next.js's fallback or a custom one in my codebase) from an API endpoint for a request/page that doesn't exist, but haven't worked a way around it in the front-end to have both suspense AND not found working.

cpotey avatar Dec 03 '25 20:12 cpotey