"Rendered more hooks than during previous render" when using App Router
Link to the code that reproduces this issue
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
- Start the application on port 3000
- Goto
<base-url>/versions/v1 - Wait for a few seconds till the error screen is displayed
- 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:-
13.4.1914.1.0
+1
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?
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}`
)
}
}
+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!
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
/**
* 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 did you solve this?
This issue happens to me when calling redirect in a page.tsx (RSC), it was fixed once I add the loading.tsx 🤷♂
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.
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?
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:
- 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.
- 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.
- 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
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.