Next.js 16 Build useState Null Error: Static Gen Fix

Next.js 16 build failing with Cannot read properties of null reading useState during static generation? Fix the React 19.2 dedupe bug with working code.
Next.jsReactBuild Errors
April 27, 20266 min read1063 words

The Problem

I picked up a contract to ship a Next.js 16 launch last week. App ran fine in dev. The first production build blew up with a wall of red:

TypeError: Cannot read properties of null (reading 'useState')
    at u (/.next/server/chunks/3815.js:1:9421)
    at U (/.next/server/chunks/3815.js:1:9890)
    at /.next/server/app/(marketing)/page.js:8:2871

Error occurred prerendering page "/"

Same error on every static route. Same error variant for useContext, useReducer, sometimes useEffect. Toggling dynamic = 'force-dynamic' made the route work but killed every static page in the build. If you upgraded to Next.js 16 with React 19.2 and your build dies on prerender with a null hooks dispatcher, this is the duplicate React copy issue and there is a clean fix.

Why It Happens

React's hooks dispatcher is a singleton. When a hook like useState runs, it reads from React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current. During render, that field gets set to the active fiber dispatcher. Outside of render, it is null.

Next.js 16 ships its own pinned copy of react and react-dom for the server build. If a dependency in your tree pulls in a second copy of React (even a matching version), the second copy's __SECRET_INTERNALS object is a different reference. When your client component renders during static generation, the bundler resolves react to one copy, the hook reads from the dispatcher, and the dispatcher on that copy was never set because Next.js initialized the other copy. The result is null.useState and the build fails.

Three things changed in 16.0.x that surface this:

Change 1: Stricter ESM resolution. Next.js 16 dropped the legacy commonjs interop shim that used to coerce duplicate React copies onto the same module record. Pre-16 builds silently ran with two copies and hoped they happened to share state. They often did not, but the error was a hydration warning, not a hard build failure.

Change 2: React 19.2 prerender expects a clean dispatcher. React 19.2 added an assertion that throws when ReactCurrentDispatcher.current is null at the moment a hook is called. Earlier React versions returned an undefined object and crashed deeper, which made the symptom look like an unrelated render error.

Change 3: useCache and prerendered client components. If you used the new 'use cache' directive in a client component and that component renders during static generation, its hooks run in the prerender phase. Any duplicate React copy in that path now throws here instead of at runtime.

The duplicate copy almost always comes from one of these:

  • A monorepo workspace where react is hoisted to root and a package also pins it locally
  • A UI library installed without peerDependencies declared correctly (older radix-ui, custom internal libs, some chart libraries)
  • pnpm with node-linker=hoisted after a partial install
  • A Yarn berry workspace where nodeLinker: pnp left a stale .pnp.cjs

The Fix

Step 1: Confirm there are two React copies. Before changing anything, prove the diagnosis:

# npm
npm ls react

# pnpm
pnpm why react

# yarn
yarn why react

If you see react@19.2.0 listed under more than one path, that is the bug. A clean tree shows react exactly once with every consumer marked as peer.

Step 2: Force dedupe at the package manager level. This is the single fix that resolves 80% of these.

For pnpm, add to .npmrc:

public-hoist-pattern[]=*react*
public-hoist-pattern[]=*react-dom*
dedupe-peer-dependents=true

Then rm -rf node_modules pnpm-lock.yaml && pnpm install. The lockfile rewrite is required, because without it pnpm reuses the old resolution graph.

For npm, add an overrides block to package.json:

{
  "overrides": {
    "react": "19.2.0",
    "react-dom": "19.2.0"
  }
}

For yarn classic, use resolutions:

{
  "resolutions": {
    "react": "19.2.0",
    "react-dom": "19.2.0"
  }
}

Re-run npm ls react after reinstall. You should see exactly one copy.

Step 3: Pin via transpilePackages if a workspace package is the culprit. When the duplicate comes from an internal package in a monorepo, tell Next.js to compile it through the same React module resolution as the app:

// next.config.ts
import type { NextConfig } from 'next'

const config: NextConfig = {
  transpilePackages: ['@acme/ui', '@acme/charts'],
  experimental: {
    optimizePackageImports: ['@acme/ui'],
  },
}

export default config

transpilePackages makes Next.js resolve react and react-dom from the consuming app, not from inside the package. This is the canonical fix for workspace UI libraries that bundle their own React reference.

Step 4: Add a build-time guard. Once the root build passes, prevent regressions. Drop this in a script and wire it to predev and prebuild:

// scripts/check-react-dupes.ts
import { execSync } from 'node:child_process'

const out = execSync('npm ls react', { encoding: 'utf8' })
const matches = out.match(/react@\d+\.\d+\.\d+/g) ?? []
const unique = new Set(matches)

if (matches.length !== unique.size || unique.size > 1) {
  console.error('Multiple React copies detected:')
  console.error(out)
  process.exit(1)
}

console.log('React dedupe ok:', [...unique].join(', '))

In package.json:

{
  "scripts": {
    "prebuild": "tsx scripts/check-react-dupes.ts",
    "build": "next build"
  }
}

Cheap insurance. CI catches the regression before deployment.

Step 5: If you still see it, dump the bundle. When dedupe is clean and the error persists, one route is bundling a stray React import. Build with the bundler analyzer and search for react chunks:

ANALYZE=true next build

The Next.js 16 bundle analyzer writes a server treemap. A correctly deduped build has one node_modules/react/ rectangle. If you see two, the second one's parent path tells you which dependency to pin.

The Lesson

The null dispatcher error in 16 is almost always two React copies, not a code bug. Dedupe at the package manager level, declare transpilePackages for workspace UI libs, and add a CI check so the duplicate cannot creep back in. Save yourself an hour of staring at stack traces.

If your Next.js 16 upgrade stalled on build errors and you want someone to land it cleanly, that is what I do — see my services, or read my Next.js 16 Server Actions invalid error writeup for another upgrade gotcha.

Back to blogStart a project