/nextjs-state-issue

Repro of buggy Next.js / React behavior involving initial state values not matching between server and client

Primary LanguageJavaScript

nextjs-state-issue

Examining a bug(?) involving populating React state from URL query params

Repro

Boot next dev server ✅

next dev

index.js just parses the URL query string for a "v" param, which is used to initialize one of 5 buttons ("C" by default):

import {useRouter} from "next/router"
import {useState} from "react";

export const pathnameRegex = /[^?#]+/u

export default function Home() {
  const searchStr = useRouter().asPath.replace(pathnameRegex, '')
  const searchParams = new URLSearchParams(searchStr)
  const initialValue = searchParams.get('v') || "C"
  const [ value, setValue ] = useState(initialValue)
  return (
    <div>{
      ["A", "B", "C", "D", "E"].map(v => {
        const disabled = v === value
        console.log(`v: ${v}, value: ${value}, disabled: ${disabled}`)
        return <input key={v} type={"button"} value={v} disabled={disabled} onClick={() => setValue(v)} />
      })
    }</div>
  )
}

View 127.0.0.1:3000

open http://127.0.0.1:3000

Page renders without error, "C" is "active" (disabled) by default:

So far, so good.

Now try 127.0.0.1:3000?v=D

Several problems are visible:

  • "C" is disabled (instead of "D")
  • console.logs imply that "D" should be disabled, and "C" should not be
  • Clicking "D" has no effect (something thinks it's already disabled)
  • There's a console error about client and server "disabled" attributes not agreeing

Click "A" ❌

  • "A" and "C" are both disabled
    • There's no way to un-disable "C"; it is stuck due to having been default during server render
  • console.logs imply only "A" should be disabled

Discussion

Issue is not specific to the disabled attribute

At first I thought this might have to do with the disabled attribute being a boolean whose value is inferred from the presence or absence of the attribute (i.e. <input type="button" [disabled] /> as opposed to <input type="button" disabled="[yes|no]" />).

However, the issue occurs if the "active" button is styled with e.g. font-weight: [bold|normal] instead of toggling disabled (see bold branch):

-        const disabled = v === value
-        console.log(`v: ${v}, value: ${value}, disabled: ${disabled}`)
-        return <input key={v} type={"button"} value={v} disabled={disabled} onClick={() => setVal>
+        const active = v === value
+        console.log(`v: ${v}, value: ${value}, active: ${active}`)
+        return <input key={v} type={"button"} value={v} style={{ fontWeight: active ? "bold" : "n>

  • Page loads with "C" bolded, though "D" is supposed to be
  • Clicking "A" results in "A" and "C" both bolded, though only "A" is supposed to be
  • One difference from the disabled example is that you can click "C" here, and it resets the page to a consistent state:
    • "C" is understood to be the only "active" button (in the UI as well as console.logs)
    • Subsequent button clicks work as expected