vercel / next.js

The React Framework
https://nextjs.org
MIT License
127.46k stars 27.04k forks source link

"Rendered more hooks than during previous render" when using `router.replace` #63121

Open mynameisankit opened 8 months ago

mynameisankit commented 8 months ago

Link to the code that reproduces this issue

Link

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
sirajtahra commented 6 months ago

+1

lifeisegg123 commented 5 months ago

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?

jetaggart commented 4 months ago

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}`
    )
  }
}
hunghuy201280 commented 2 months ago

+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!