-
Notifications
You must be signed in to change notification settings - Fork 402
fix(nextjs): Improve support for cacheComponents #7119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@clerk/nextjs': patch | ||
| --- | ||
|
|
||
| Improve support for apps that opt-in to using Next.js new `cacheComponents` feature. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,6 @@ import { canUseKeyless } from '../../utils/feature-flags'; | |
| import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; | ||
| import { RouterTelemetry } from '../../utils/router-telemetry'; | ||
| import { isNextWithUnstableServerActions } from '../../utils/sdk-versions'; | ||
| import { detectKeylessEnvDriftAction } from '../keyless-actions'; | ||
| import { invalidateCacheAction } from '../server-actions'; | ||
| import { useAwaitablePush } from './useAwaitablePush'; | ||
| import { useAwaitableReplace } from './useAwaitableReplace'; | ||
|
|
@@ -28,6 +27,11 @@ const LazyCreateKeylessApplication = dynamic(() => | |
| import('./keyless-creator-reader.js').then(m => m.KeylessCreatorOrReader), | ||
| ); | ||
|
|
||
| /** | ||
| * LazyKeylessDriftDetector should only be loaded if the conditions below are met. | ||
| */ | ||
| const LazyKeylessDriftDetector = dynamic(() => import('./keyless-drift-detector.js').then(m => m.KeylessDriftDetector)); | ||
|
|
||
| const NextClientClerkProvider = (props: NextClerkProviderProps) => { | ||
| if (isNextWithUnstableServerActions) { | ||
| const deprecationWarning = `Clerk:\nYour current Next.js version (${nextPackage.version}) will be deprecated in the next major release of "@clerk/nextjs". Please upgrade to [email protected] or later.`; | ||
|
|
@@ -44,13 +48,6 @@ const NextClientClerkProvider = (props: NextClerkProviderProps) => { | |
| const replace = useAwaitableReplace(); | ||
| const [isPending, startTransition] = useTransition(); | ||
|
|
||
| // Call drift detection on mount (client-side) | ||
| useSafeLayoutEffect(() => { | ||
| if (canUseKeyless) { | ||
| void detectKeylessEnvDriftAction(); | ||
| } | ||
| }, []); | ||
|
|
||
| // Avoid rendering nested ClerkProviders by checking for the existence of the ClerkNextOptions context provider | ||
| const isNested = Boolean(useClerkNextOptions()); | ||
| if (isNested) { | ||
|
|
@@ -137,12 +134,19 @@ const NextClientClerkProvider = (props: NextClerkProviderProps) => { | |
| ); | ||
| }; | ||
|
|
||
| export const ClientClerkProvider = (props: NextClerkProviderProps & { disableKeyless?: boolean }) => { | ||
| const { children, disableKeyless = false, ...rest } = props; | ||
| export const ClientClerkProvider = ( | ||
| props: NextClerkProviderProps & { disableKeyless?: boolean; disableKeylessDriftDetection?: boolean }, | ||
| ) => { | ||
| const { children, disableKeyless = false, disableKeylessDriftDetection = false, ...rest } = props; | ||
| const safePublishableKey = mergeNextClerkPropsWithEnv(rest).publishableKey; | ||
|
|
||
| if (safePublishableKey || !canUseKeyless || disableKeyless) { | ||
| return <NextClientClerkProvider {...rest}>{children}</NextClientClerkProvider>; | ||
| return ( | ||
| <NextClientClerkProvider {...rest}> | ||
| {!disableKeylessDriftDetection && <LazyKeylessDriftDetector />} | ||
| {children} | ||
| </NextClientClerkProvider> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { useSelectedLayoutSegments } from 'next/navigation'; | ||
| import { useEffect } from 'react'; | ||
|
|
||
| import { canUseKeyless } from '../../utils/feature-flags'; | ||
|
|
||
| export function KeylessDriftDetector() { | ||
| const segments = useSelectedLayoutSegments(); | ||
| const isNotFoundRoute = segments[0]?.startsWith('/_not-found') || false; | ||
|
|
||
| useEffect(() => { | ||
| if (canUseKeyless && !isNotFoundRoute) { | ||
| void import('../keyless-actions.js').then(m => m.detectKeylessEnvDriftAction()); | ||
| } | ||
| }, [isNotFoundRoute]); | ||
|
|
||
| return null; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,8 +7,10 @@ import { PromisifiedAuthProvider } from '../../client-boundary/PromisifiedAuthPr | |||||||||||||||||||||||||
| import { getDynamicAuthData } from '../../server/buildClerkProps'; | ||||||||||||||||||||||||||
| import type { NextClerkProviderProps } from '../../types'; | ||||||||||||||||||||||||||
| import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; | ||||||||||||||||||||||||||
| import { onlyTry } from '../../utils/only-try'; | ||||||||||||||||||||||||||
| import { isNext13 } from '../../utils/sdk-versions'; | ||||||||||||||||||||||||||
| import { ClientClerkProvider } from '../client/ClerkProvider'; | ||||||||||||||||||||||||||
| import { SuspenseWhenCached } from '../suspense-when-cached'; | ||||||||||||||||||||||||||
| import { getKeylessStatus, KeylessProvider } from './keyless-provider'; | ||||||||||||||||||||||||||
| import { buildRequestLike, getScriptNonceFromHeader } from './utils'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -65,39 +67,48 @@ export async function ClerkProvider( | |||||||||||||||||||||||||
| ...rest, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const { shouldRunAsKeyless, runningWithClaimedKeys } = await getKeylessStatus(propsWithEnvs); | ||||||||||||||||||||||||||
| const { shouldRunAsKeyless, runningWithClaimedKeys, runningWithDriftedKeys } = await getKeylessStatus(propsWithEnvs); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let output: ReactNode; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then( | ||||||||||||||||||||||||||
| mod => mod.detectKeylessEnvDrift, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| await detectKeylessEnvDrift(); | ||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||
| // ignore | ||||||||||||||||||||||||||
| if (runningWithDriftedKeys) { | ||||||||||||||||||||||||||
| onlyTry(async () => { | ||||||||||||||||||||||||||
| const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then( | ||||||||||||||||||||||||||
| mod => mod.detectKeylessEnvDrift, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| await detectKeylessEnvDrift(); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
Comment on lines
+72
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid wrapping async drift detection in
- if (runningWithDriftedKeys) {
- onlyTry(async () => {
- const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then(
- mod => mod.detectKeylessEnvDrift,
- );
- await detectKeylessEnvDrift();
- });
- }
+ if (runningWithDriftedKeys) {
+ void import('../../server/keyless-telemetry.js')
+ .then(mod => mod.detectKeylessEnvDrift?.())
+ .catch(() => {});
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let output: ReactNode; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (shouldRunAsKeyless) { | ||||||||||||||||||||||||||
| output = ( | ||||||||||||||||||||||||||
| <KeylessProvider | ||||||||||||||||||||||||||
| rest={propsWithEnvs} | ||||||||||||||||||||||||||
| generateNonce={generateNonce} | ||||||||||||||||||||||||||
| generateStatePromise={generateStatePromise} | ||||||||||||||||||||||||||
| runningWithClaimedKeys={runningWithClaimedKeys} | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||
| </KeylessProvider> | ||||||||||||||||||||||||||
| <SuspenseWhenCached> | ||||||||||||||||||||||||||
| <KeylessProvider | ||||||||||||||||||||||||||
| rest={propsWithEnvs} | ||||||||||||||||||||||||||
| generateNonce={generateNonce} | ||||||||||||||||||||||||||
| generateStatePromise={generateStatePromise} | ||||||||||||||||||||||||||
| runningWithClaimedKeys={runningWithClaimedKeys} | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||
| </KeylessProvider> | ||||||||||||||||||||||||||
| </SuspenseWhenCached> | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| output = ( | ||||||||||||||||||||||||||
| <ClientClerkProvider | ||||||||||||||||||||||||||
| {...propsWithEnvs} | ||||||||||||||||||||||||||
| nonce={await generateNonce()} | ||||||||||||||||||||||||||
| initialState={await generateStatePromise()} | ||||||||||||||||||||||||||
| // This suspense boundary is required because `cacheComponents` does not like the fact that we await `generateNonce` even though it does not accesss runtime APIs. | ||||||||||||||||||||||||||
| <SuspenseWhenCached | ||||||||||||||||||||||||||
| // When dynamic is true, we don't want to ever wrap with Suspense, instead we should let the developer handle it. | ||||||||||||||||||||||||||
| noopWhen={dynamic} | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||
| </ClientClerkProvider> | ||||||||||||||||||||||||||
| <ClientClerkProvider | ||||||||||||||||||||||||||
| {...propsWithEnvs} | ||||||||||||||||||||||||||
| nonce={await generateNonce()} | ||||||||||||||||||||||||||
| initialState={await generateStatePromise()} | ||||||||||||||||||||||||||
| disableKeylessDriftDetection | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||
| </ClientClerkProvider> | ||||||||||||||||||||||||||
| </SuspenseWhenCached> | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,19 +16,29 @@ import { deleteKeylessAction } from '../keyless-actions'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function getKeylessStatus( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| params: Without<NextClerkProviderProps, '__unstable_invokeMiddlewareOnAuthStateChange'>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let [shouldRunAsKeyless, runningWithClaimedKeys, locallyStoredPublishableKey] = [false, false, '']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let [shouldRunAsKeyless, runningWithClaimedKeys, runningWithDriftedKeys, locallyStoredPublishableKey] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (canUseKeyless) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| locallyStoredPublishableKey = await import('../../server/keyless-node.js') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then(mod => mod.safeParseClerkFile()?.publishableKey || '') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch(() => ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runningWithClaimedKeys = Boolean(params.publishableKey) && params.publishableKey === locallyStoredPublishableKey; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runningWithDriftedKeys = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Boolean(params.publishableKey) && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Boolean(locallyStoredPublishableKey) && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| params.publishableKey !== locallyStoredPublishableKey; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shouldRunAsKeyless = !params.publishableKey || runningWithClaimedKeys; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shouldRunAsKeyless, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runningWithClaimedKeys, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runningWithDriftedKeys, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
16
to
43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Add explicit return type annotation for public API. The As per coding guidelines Apply this diff to add an explicit return type: -export async function getKeylessStatus(
+export async function getKeylessStatus(
params: Without<NextClerkProviderProps, '__unstable_invokeMiddlewareOnAuthStateChange'>,
-) {
+): Promise<{
+ shouldRunAsKeyless: boolean;
+ runningWithClaimedKeys: boolean;
+ runningWithDriftedKeys: boolean;
+}> {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -59,6 +69,7 @@ export const KeylessProvider = async (props: KeylessProviderProps) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nonce={await generateNonce()} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| initialState={await generateStatePromise()} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disableKeyless | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disableKeylessDriftDetection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ClientClerkProvider> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -77,6 +88,7 @@ export const KeylessProvider = async (props: KeylessProviderProps) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nonce={await generateNonce()} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| initialState={await generateStatePromise()} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disableKeylessDriftDetection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ClientClerkProvider> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { type PropsWithChildren, Suspense } from 'react'; | ||
| import React from 'react'; | ||
|
|
||
| // eslint-disable-next-line turbo/no-undeclared-env-vars | ||
| const isNextCacheComponents = process.env.__NEXT_CACHE_COMPONENTS; | ||
|
|
||
| /** | ||
| * Wraps the children in a Suspense component if the current environment is a Next.js cache component. | ||
| * @param children - The children to render. | ||
| * @param noopWhen - A condition to opt out of wrapping with Suspense. | ||
| */ | ||
| export const SuspenseWhenCached = ({ | ||
| children, | ||
| noopWhen = false, | ||
| }: PropsWithChildren<{ | ||
| /** | ||
| * A condition to opt out of wrapping with Suspense. | ||
| */ | ||
| noopWhen?: boolean; | ||
| }>) => { | ||
| if (!isNextCacheComponents) { | ||
| return children; | ||
| } | ||
|
|
||
| if (noopWhen) { | ||
| return children; | ||
| } | ||
|
|
||
| return <Suspense>{children}</Suspense>; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add the
'use client';directiveThis component uses client-only hooks (
useSelectedLayoutSegments,useEffect), but without the'use client';directive Next.js will treat it as a server component and fail at build/runtime with “Hooks can only be used in a Client Component” errors. Please add the directive so the file is compiled as a client component.📝 Committable suggestion
🤖 Prompt for AI Agents