Skip to content

fix: rewrite error for react hooks without directive#844

Merged
james-elicx merged 3 commits intomainfrom
james/react-use-client-hook
Apr 14, 2026
Merged

fix: rewrite error for react hooks without directive#844
james-elicx merged 3 commits intomainfrom
james/react-use-client-hook

Conversation

@james-elicx
Copy link
Copy Markdown
Collaborator

fixes #834

@james-elicx
Copy link
Copy Markdown
Collaborator Author

/bigbonk review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 14, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@844

commit: adc75b7

@github-actions
Copy link
Copy Markdown

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good approach — rewriting the cryptic "useState is not a function" into an actionable error message is the right call, and the test coverage + fixture look solid.

Two issues to address:

1. Missing hooks in the regex pattern

The regex catches 11 client-only hooks, but React 19 has 14 hooks absent from the react-server export condition. Three are missing:

  • useActionState
  • useOptimistic
  • useEffectEvent

All three will produce the same "X is not a function" TypeError on the server but won't be rewritten by the current pattern. They should be added.

2. Consider reusing throwClientHookError instead of duplicating the message

The error message string is now defined in two places:

  1. shims/client-hook-error.ts:10-13throwClientHookError()
  2. server/app-page-boundary-render.ts:412-414rewriteClientHookError()

These serve different purposes (throw-on-call vs. rewrite-after-catch), so having two functions is fine, but the message template itself could be extracted into a shared constant or the rewrite function could import and use the same format helper. This is a minor nit — if you'd prefer to keep them separate for clarity, a comment cross-referencing the two locations would be enough. Just want to avoid them drifting apart silently.

// "useState is not a function". Rewrite into an actionable message matching
// the format used by the next/navigation shims (see client-hook-error.ts).
const _clientHookPattern =
/\b(useState|useEffect|useReducer|useRef|useContext|useLayoutEffect|useInsertionEffect|useSyncExternalStore|useTransition|useImperativeHandle|useDeferredValue)\b.*is not a function/;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three React 19 client-only hooks are missing from this pattern: useActionState, useOptimistic, and useEffectEvent. All three are absent from the react-server export condition in React 19.2.5 and will produce the same "X is not a function" TypeError when called in a Server Component.

Suggested change
/\b(useState|useEffect|useReducer|useRef|useContext|useLayoutEffect|useInsertionEffect|useSyncExternalStore|useTransition|useImperativeHandle|useDeferredValue)\b.*is not a function/;
const _clientHookPattern =
/\b(useState|useEffect|useReducer|useRef|useContext|useLayoutEffect|useInsertionEffect|useSyncExternalStore|useTransition|useImperativeHandle|useDeferredValue|useActionState|useOptimistic|useEffectEvent)\b.*is not a function/;

`${match[1]}() only works in Client Components. Add the "use client" directive ` +
`at the top of the file to use it. Read more: ` +
`https://nextjs.org/docs/messages/react-client-hook-in-server-component`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this message template is duplicated from shims/client-hook-error.ts:10-13. Consider extracting the format string into a shared helper (e.g., buildClientHookErrorMessage(hookName) in client-hook-error.ts) that both throwClientHookError and rewriteClientHookError can call. That way the wording stays in sync if it ever changes.

Alternatively, just adding a // Keep in sync with shims/client-hook-error.ts comment here would be fine.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Apr 14, 2026

Review posted successfully on PR #844. Here's a summary of the feedback:

Request Changes with two items:

  1. Missing hooks in the regex pattern — The regex catches 11 of the 14 React hooks absent from the react-server export condition. Three React 19 hooks are missing: useActionState, useOptimistic, and useEffectEvent. I provided a suggestion with the complete list.

  2. Duplicated error message template (minor nit) — The error message format is now in two places (shims/client-hook-error.ts and the new rewriteClientHookError). Suggested either extracting a shared format helper or adding a cross-reference comment to keep them in sync.

github run

@james-elicx
Copy link
Copy Markdown
Collaborator Author

/bigbonk re-review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both items from the previous review are addressed:

  1. Hooks list is now complete. All 14 React hooks absent from the react-server export condition are present in the regex. Verified against the actual react@19.2.5 package exports — the list matches exactly. react-dom hooks (useFormStatus, useFormState) are correctly excluded since they come from a different import path and would produce a different error pattern.

  2. Shared error message. buildClientHookErrorMessage is properly extracted in client-hook-error.ts and imported by both throwClientHookError (shim-time throw) and rewriteClientHookError (catch-time rewrite). Single source of truth.

Coverage analysis: rewriteClientHookError is called in renderAppPageErrorBoundary, which is the convergence point for all user-facing App Router error rendering — buildAppPageElement catch, SSR stream recovery catch, and RSC stream global error re-render all flow through it. Pages Router paths are not applicable (no Server Components, so all hooks are available).

One minor DX note (not blocking): the rscOnError callback in the generated RSC entry logs errors to the server console without the rewrite, so server-side logs during RSC streaming would still show the raw "useState is not a function" message. Only affects developer log inspection, not the browser-facing error. Could be a follow-up.

Test and fixture look good. LGTM.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Apr 14, 2026

Approved PR #844. Here's a summary of the review:

Approved — both items from the previous review are addressed:

  1. Hooks list is complete. Verified against the actual react@19.2.5 react-server exports — all 14 client-only hooks are in the regex. react-dom hooks are correctly excluded (different import path, different error pattern).

  2. Shared error message. buildClientHookErrorMessage is extracted and imported by both call sites — no duplication.

Coverage analysis: rewriteClientHookError sits in renderAppPageErrorBoundary, which is the convergence point for all user-facing App Router error paths (element build catch, SSR stream recovery, RSC global error re-render). Pages Router paths don't apply since there's no react-server condition stripping hooks.

One minor non-blocking note: The rscOnError callback in the RSC entry logs errors to server console without the rewrite, so server-side logs would still show the raw "useState is not a function". Only affects developer log inspection, not browser-facing errors — could be a follow-up.

github run

@james-elicx james-elicx enabled auto-merge (squash) April 14, 2026 18:08
@james-elicx james-elicx merged commit e46b9ba into main Apr 14, 2026
32 checks passed
@james-elicx james-elicx deleted the james/react-use-client-hook branch April 14, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some hooks silently become undefined without an error when 'use client' is missing

1 participant