From 95ac67a143c263bef0c1f589728566ab8f95768d Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Wed, 4 Sep 2024 15:35:28 -0500 Subject: [PATCH] feat(elements): Remove direct reliance on next and clerk-react (#4064) --- .changeset/good-jeans-call.md | 7 +++ package-lock.json | 2 - packages/elements/package.json | 2 - .../elements/src/internals/constants/index.ts | 15 +++++-- .../machines/shared/shared.actors.ts | 2 +- .../utils/inspector/browser/index.ts | 6 ++- .../utils/inspector/console/index.ts | 8 +++- .../elements/src/react/common/form/input.tsx | 2 +- .../elements/src/react/common/loading.tsx | 2 +- .../src/react/hooks/use-password.hook.ts | 2 +- .../hooks/use-third-party-provider.hook.ts | 2 +- packages/elements/src/react/router/index.ts | 1 - packages/elements/src/react/router/next.ts | 32 -------------- packages/elements/src/react/sign-in/root.tsx | 20 ++++++--- packages/elements/src/react/sign-in/step.tsx | 2 +- packages/elements/src/react/sign-up/root.tsx | 20 ++++++--- packages/elements/src/react/sign-up/step.tsx | 2 +- packages/elements/src/utils/safe-access.ts | 7 +++ .../src/app-router/client/ClerkProvider.tsx | 43 +++++++++++++++++-- packages/shared/src/react/contexts.tsx | 13 ++++++ packages/shared/src/react/index.ts | 4 +- packages/shared/src/router.ts | 9 +++- packages/shared/src/router/react.tsx | 18 +++++++- 23 files changed, 149 insertions(+), 72 deletions(-) create mode 100644 .changeset/good-jeans-call.md delete mode 100644 packages/elements/src/react/router/next.ts create mode 100644 packages/elements/src/utils/safe-access.ts diff --git a/.changeset/good-jeans-call.md b/.changeset/good-jeans-call.md new file mode 100644 index 0000000000..f0b0048f7e --- /dev/null +++ b/.changeset/good-jeans-call.md @@ -0,0 +1,7 @@ +--- +"@clerk/elements": minor +"@clerk/nextjs": minor +"@clerk/shared": minor +--- + +Remove `@clerk/elements` reliance on `next` and `@clerk/clerk-react` directly. The host router is now provided by `@clerk/nextjs`. diff --git a/package-lock.json b/package-lock.json index 60348ca245..bcb1421a8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44183,9 +44183,7 @@ "node": ">=18.17.0" }, "peerDependencies": { - "@clerk/clerk-react": "^5.0.0", "@clerk/shared": "^2.0.0", - "next": "^13.5.4 || ^14.0.3 || ^15.0.0-rc", "react": "^18.0.0 || ^19.0.0-beta", "react-dom": "^18.0.0 || ^19.0.0-beta" }, diff --git a/packages/elements/package.json b/packages/elements/package.json index 4dde6998a7..0078700836 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -94,9 +94,7 @@ "typescript": "*" }, "peerDependencies": { - "@clerk/clerk-react": "^5.0.0", "@clerk/shared": "^2.0.0", - "next": "^13.5.4 || ^14.0.3 || ^15.0.0-rc", "react": "^18.0.0 || ^19.0.0-beta", "react-dom": "^18.0.0 || ^19.0.0-beta" }, diff --git a/packages/elements/src/internals/constants/index.ts b/packages/elements/src/internals/constants/index.ts index f4777a9ee2..1ca489d3c2 100644 --- a/packages/elements/src/internals/constants/index.ts +++ b/packages/elements/src/internals/constants/index.ts @@ -1,11 +1,18 @@ +import { safeAccess } from '~/utils/safe-access'; + export const SSO_CALLBACK_PATH_ROUTE = '/sso-callback'; export const CHOOSE_SESSION_PATH_ROUTE = '/choose'; export const MAGIC_LINK_VERIFY_PATH_ROUTE = '/verify'; -export const SIGN_IN_DEFAULT_BASE_PATH = - process.env.CLERK_SIGN_IN_URL ?? process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL ?? '/sign-in'; -export const SIGN_UP_DEFAULT_BASE_PATH = - process.env.CLERK_SIGN_UP_URL ?? process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL ?? '/sign-up'; +// TODO: remove reliance on next-specific variables here +export const SIGN_IN_DEFAULT_BASE_PATH = safeAccess( + () => process.env.CLERK_SIGN_IN_URL ?? process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL, + '/sign-in', +); +export const SIGN_UP_DEFAULT_BASE_PATH = safeAccess( + () => process.env.CLERK_SIGN_UP_URL ?? process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL, + '/sign-up', +); // The version that Next added support for the window.history.pushState and replaceState APIs. // ref: https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate diff --git a/packages/elements/src/internals/machines/shared/shared.actors.ts b/packages/elements/src/internals/machines/shared/shared.actors.ts index 61f7692e40..447ef67e32 100644 --- a/packages/elements/src/internals/machines/shared/shared.actors.ts +++ b/packages/elements/src/internals/machines/shared/shared.actors.ts @@ -10,7 +10,7 @@ export const clerkLoader = fromCallback(({ sen if (clerk.loaded) { reportLoaded(); } else if ('addOnLoaded' in clerk) { - // @ts-expect-error - Expects `addOnLoaded` from @clerk/clerk-react's IsomorphicClerk. + // @ts-expect-error - Expects `addOnLoaded` from @clerk/shared/react's IsomorphicClerk. clerk.addOnLoaded(reportLoaded); } else { sendBack({ type: 'ERROR', message: 'Clerk client could not be loaded' }); diff --git a/packages/elements/src/internals/utils/inspector/browser/index.ts b/packages/elements/src/internals/utils/inspector/browser/index.ts index ba3d0ff2d0..30f12a3e82 100644 --- a/packages/elements/src/internals/utils/inspector/browser/index.ts +++ b/packages/elements/src/internals/utils/inspector/browser/index.ts @@ -1,12 +1,16 @@ import { isTruthy } from '@clerk/shared/underscore'; import { createBrowserInspector } from '@statelyai/inspect'; +import { safeAccess } from '~/utils/safe-access'; + export const getInspector = () => { if ( __DEV__ && typeof window !== 'undefined' && process.env.NODE_ENV === 'development' && - isTruthy(process.env.NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG_UI ?? process.env.CLERK_ELEMENTS_DEBUG_UI) + isTruthy( + safeAccess(() => process.env.NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG_UI ?? process.env.CLERK_ELEMENTS_DEBUG_UI, false), + ) ) { const { inspect } = createBrowserInspector({ autoStart: true, diff --git a/packages/elements/src/internals/utils/inspector/console/index.ts b/packages/elements/src/internals/utils/inspector/console/index.ts index 62385651a6..1af63ce18d 100644 --- a/packages/elements/src/internals/utils/inspector/console/index.ts +++ b/packages/elements/src/internals/utils/inspector/console/index.ts @@ -1,16 +1,20 @@ import { isTruthy } from '@clerk/shared/underscore'; +import { safeAccess } from '~/utils/safe-access'; + import { createConsoleInspector } from './console'; export function getInspector() { if ( __DEV__ && process.env.NODE_ENV === 'development' && - isTruthy(process.env.NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG ?? process.env.CLERK_ELEMENTS_DEBUG) + isTruthy( + safeAccess(() => process.env.NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG_UI ?? process.env.CLERK_ELEMENTS_DEBUG_UI, false), + ) ) { return createConsoleInspector({ enabled: true, - debugServer: isTruthy(process.env.CLERK_ELEMENTS_DEBUG_SERVER), + debugServer: isTruthy(safeAccess(() => process.env.CLERK_ELEMENTS_DEBUG_SERVER, false)), }); } return undefined; diff --git a/packages/elements/src/react/common/form/input.tsx b/packages/elements/src/react/common/form/input.tsx index cfda518191..07a09af1c5 100644 --- a/packages/elements/src/react/common/form/input.tsx +++ b/packages/elements/src/react/common/form/input.tsx @@ -1,5 +1,5 @@ -import { useClerk } from '@clerk/clerk-react'; import { logger } from '@clerk/shared/logger'; +import { useClerk } from '@clerk/shared/react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; import type { Control as RadixControl, diff --git a/packages/elements/src/react/common/loading.tsx b/packages/elements/src/react/common/loading.tsx index 370a241cb1..d9937cf893 100644 --- a/packages/elements/src/react/common/loading.tsx +++ b/packages/elements/src/react/common/loading.tsx @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; import type { OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types'; import { useSelector } from '@xstate/react'; diff --git a/packages/elements/src/react/hooks/use-password.hook.ts b/packages/elements/src/react/hooks/use-password.hook.ts index d5b5860bc4..f7138b3eb2 100644 --- a/packages/elements/src/react/hooks/use-password.hook.ts +++ b/packages/elements/src/react/hooks/use-password.hook.ts @@ -1,5 +1,5 @@ -import { useClerk } from '@clerk/clerk-react'; import { noop } from '@clerk/shared'; +import { useClerk } from '@clerk/shared/react'; import type { PasswordSettingsData, PasswordValidation } from '@clerk/types'; import * as React from 'react'; diff --git a/packages/elements/src/react/hooks/use-third-party-provider.hook.ts b/packages/elements/src/react/hooks/use-third-party-provider.hook.ts index db199e914f..565112b15b 100644 --- a/packages/elements/src/react/hooks/use-third-party-provider.hook.ts +++ b/packages/elements/src/react/hooks/use-third-party-provider.hook.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import type { OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types'; import type React from 'react'; import { useCallback } from 'react'; diff --git a/packages/elements/src/react/router/index.ts b/packages/elements/src/react/router/index.ts index 5b0979e8a2..3b037aa70a 100644 --- a/packages/elements/src/react/router/index.ts +++ b/packages/elements/src/react/router/index.ts @@ -1,3 +1,2 @@ -export { useNextRouter } from './next'; export { Route, Router, useClerkRouter } from '@clerk/shared/router'; export { useVirtualRouter } from './virtual'; diff --git a/packages/elements/src/react/router/next.ts b/packages/elements/src/react/router/next.ts deleted file mode 100644 index d8a8ac96a1..0000000000 --- a/packages/elements/src/react/router/next.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ClerkHostRouter } from '@clerk/shared/router'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; - -import { NEXT_WINDOW_HISTORY_SUPPORT_VERSION } from '~/internals/constants'; - -/** - * Clerk router integration with Next.js's router. - */ -export const useNextRouter = (): ClerkHostRouter => { - const router = useRouter(); - const pathname = usePathname(); - // eslint-disable-next-line react-hooks/rules-of-hooks -- The order doesn't differ between renders as we're checking the execution environment. - const searchParams = typeof window === 'undefined' ? new URLSearchParams() : useSearchParams(); - - // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to - // preserve internal state between steps. - const canUseWindowHistoryAPIs = - typeof window !== 'undefined' && window.next && window.next.version >= NEXT_WINDOW_HISTORY_SUPPORT_VERSION; - - return { - mode: 'path', - name: 'NextRouter', - push: (path: string) => router.push(path), - replace: (path: string) => - canUseWindowHistoryAPIs ? window.history.replaceState(null, '', path) : router.replace(path), - shallowPush(path: string) { - canUseWindowHistoryAPIs ? window.history.pushState(null, '', path) : router.push(path, {}); - }, - pathname: () => pathname, - searchParams: () => searchParams, - }; -}; diff --git a/packages/elements/src/react/sign-in/root.tsx b/packages/elements/src/react/sign-in/root.tsx index 94292b8c54..e844cd3eba 100644 --- a/packages/elements/src/react/sign-in/root.tsx +++ b/packages/elements/src/react/sign-in/root.tsx @@ -1,4 +1,5 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; +import { useClerkHostRouter } from '@clerk/shared/router'; import { eventComponentMounted } from '@clerk/shared/telemetry'; import { useSelector } from '@xstate/react'; import React, { useEffect } from 'react'; @@ -9,7 +10,7 @@ import { FormStoreProvider, useFormStore } from '~/internals/machines/form/form. import type { SignInRouterInitEvent } from '~/internals/machines/sign-in'; import { SignInRouterMachine } from '~/internals/machines/sign-in'; import { inspect } from '~/internals/utils/inspector'; -import { Router, useClerkRouter, useNextRouter, useVirtualRouter } from '~/react/router'; +import { Router, useClerkRouter, useVirtualRouter } from '~/react/router'; import { SignInRouterCtx } from '~/react/sign-in/context'; import { Form } from '../common/form'; @@ -39,8 +40,7 @@ function SignInFlowProvider({ children, exampleMode, fallback, isRootPath }: Sig return; } - // @ts-expect-error -- This is actually an IsomorphicClerk instance - clerk.addOnLoaded(() => { + const cb = () => { const evt: SignInRouterInitEvent = { type: 'INIT', clerk, @@ -53,7 +53,14 @@ function SignInFlowProvider({ children, exampleMode, fallback, isRootPath }: Sig if (actor.getSnapshot().can(evt)) { actor.send(evt); } - }); + }; + + if ('addOnLoaded' in clerk) { + // @ts-expect-error - addOnLoaded doesn't exist on the clerk type, but it does on IsomorphicClerk, which can be hit when Elements is used standalone + clerk.addOnLoaded(cb); + } else { + cb(); + } // Ensure that the latest instantiated formRef is attached to the router if (formRef && actor.getSnapshot().can({ type: 'RESET.STEP' })) { @@ -123,8 +130,7 @@ export function SignInRoot({ }), ); - // TODO: eventually we'll rely on the framework SDK to specify its host router, but for now we'll default to Next.js - const router = (routing === ROUTING.virtual ? useVirtualRouter : useNextRouter)(); + const router = (routing === ROUTING.virtual ? useVirtualRouter : useClerkHostRouter)(); const isRootPath = path === router.pathname(); return ( diff --git a/packages/elements/src/react/sign-in/step.tsx b/packages/elements/src/react/sign-in/step.tsx index c01a08b02b..89a5be4f9c 100644 --- a/packages/elements/src/react/sign-in/step.tsx +++ b/packages/elements/src/react/sign-in/step.tsx @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; import { ClerkElementsRuntimeError } from '~/internals/errors'; diff --git a/packages/elements/src/react/sign-up/root.tsx b/packages/elements/src/react/sign-up/root.tsx index 9cea15248d..e080d731fc 100644 --- a/packages/elements/src/react/sign-up/root.tsx +++ b/packages/elements/src/react/sign-up/root.tsx @@ -1,4 +1,5 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; +import { useClerkHostRouter } from '@clerk/shared/router'; import { eventComponentMounted } from '@clerk/shared/telemetry'; import { useSelector } from '@xstate/react'; import { useEffect } from 'react'; @@ -9,7 +10,7 @@ import { FormStoreProvider, useFormStore } from '~/internals/machines/form/form. import type { SignUpRouterInitEvent } from '~/internals/machines/sign-up'; import { SignUpRouterMachine } from '~/internals/machines/sign-up'; import { inspect } from '~/internals/utils/inspector'; -import { Router, useClerkRouter, useNextRouter, useVirtualRouter } from '~/react/router'; +import { Router, useClerkRouter, useVirtualRouter } from '~/react/router'; import { SignUpRouterCtx } from '~/react/sign-up/context'; import { Form } from '../common/form'; @@ -39,8 +40,7 @@ function SignUpFlowProvider({ children, exampleMode, fallback, isRootPath }: Sig return; } - // @ts-expect-error -- This is actually an IsomorphicClerk instance - clerk.addOnLoaded(() => { + const cb = () => { const evt: SignUpRouterInitEvent = { type: 'INIT', clerk, @@ -61,7 +61,14 @@ function SignUpFlowProvider({ children, exampleMode, fallback, isRootPath }: Sig formRef, }); } - }); + }; + + if ('addOnLoaded' in clerk) { + // @ts-expect-error - addOnLoaded doesn't exist on the clerk type, but it does on IsomorphicClerk, which can be hit when Elements is used standalone + clerk.addOnLoaded(cb); + } else { + cb(); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [clerk, exampleMode, formRef?.id, !!router, clerk.loaded]); @@ -122,8 +129,7 @@ export function SignUpRoot({ }), ); - // TODO: eventually we'll rely on the framework SDK to specify its host router, but for now we'll default to Next.js - const router = (routing === ROUTING.virtual ? useVirtualRouter : useNextRouter)(); + const router = (routing === ROUTING.virtual ? useVirtualRouter : useClerkHostRouter)(); const isRootPath = path === router.pathname(); return ( diff --git a/packages/elements/src/react/sign-up/step.tsx b/packages/elements/src/react/sign-up/step.tsx index cd7c7788e1..d17e72c567 100644 --- a/packages/elements/src/react/sign-up/step.tsx +++ b/packages/elements/src/react/sign-up/step.tsx @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; import { ClerkElementsRuntimeError } from '~/internals/errors'; diff --git a/packages/elements/src/utils/safe-access.ts b/packages/elements/src/utils/safe-access.ts new file mode 100644 index 0000000000..8813fbc6a6 --- /dev/null +++ b/packages/elements/src/utils/safe-access.ts @@ -0,0 +1,7 @@ +export function safeAccess(fn: any, fallback: any) { + try { + return fn(); + } catch (e) { + return fallback; + } +} diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index debb2b2a7a..d9c1f9cf33 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -1,6 +1,8 @@ 'use client'; import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; -import { useRouter } from 'next/navigation'; +import type { ClerkHostRouter } from '@clerk/shared/router'; +import { ClerkHostRouterContext } from '@clerk/shared/router'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import React, { useEffect, useTransition } from 'react'; import { useSafeLayoutEffect } from '../../client-boundary/hooks/useSafeLayoutEffect'; @@ -17,12 +19,48 @@ declare global { __clerk_nav_await: Array<(value: void) => void>; __clerk_nav: (to: string) => Promise; __clerk_internal_invalidateCachePromise: () => void | undefined; + next?: { + version: string; + }; } } +// The version that Next added support for the window.history.pushState and replaceState APIs. +// ref: https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate +export const NEXT_WINDOW_HISTORY_SUPPORT_VERSION = '14.1.0'; + +/** + * Clerk router integration with Next.js's router. + */ +export const useNextRouter = (): ClerkHostRouter => { + const router = useRouter(); + const pathname = usePathname(); + // eslint-disable-next-line react-hooks/rules-of-hooks -- The order doesn't differ between renders as we're checking the execution environment. + const searchParams = typeof window === 'undefined' ? new URLSearchParams() : useSearchParams(); + + // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to + // preserve internal state between steps. + const canUseWindowHistoryAPIs = + typeof window !== 'undefined' && window.next && window.next.version >= NEXT_WINDOW_HISTORY_SUPPORT_VERSION; + + return { + mode: 'path', + name: 'NextRouter', + push: (path: string) => router.push(path), + replace: (path: string) => + canUseWindowHistoryAPIs ? window.history.replaceState(null, '', path) : router.replace(path), + shallowPush(path: string) { + canUseWindowHistoryAPIs ? window.history.pushState(null, '', path) : router.push(path, {}); + }, + pathname: () => pathname, + searchParams: () => searchParams, + }; +}; + export const ClientClerkProvider = (props: NextClerkProviderProps) => { const { __unstable_invokeMiddlewareOnAuthStateChange = true, children } = props; const router = useRouter(); + const clerkRouter = useNextRouter(); const push = useAwaitablePush(); const replace = useAwaitableReplace(); const [isPending, startTransition] = useTransition(); @@ -57,7 +95,6 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => { return new Promise(res => { window.__clerk_internal_invalidateCachePromise = res; startTransition(() => { - //@ts-expect-error next exists on window if (window.next?.version && typeof window.next.version === 'string' && window.next.version.startsWith('13')) { router.refresh(); } else { @@ -84,7 +121,7 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => { - {children} + {children} ); diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index 51b9bab76d..d8965507ab 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -2,6 +2,7 @@ import type { ActiveSessionResource, + ClerkOptions, ClientResource, LoadedClerk, OrganizationResource, @@ -20,6 +21,16 @@ const [SessionContext, useSessionContext] = createContextAndHook({}); + +function useOptionsContext(): ClerkOptions { + const context = React.useContext(OptionsContext); + if (context === undefined) { + throw new Error('useOptions must be used within an OptionsContext'); + } + return context; +} + type OrganizationContextProps = { organization: OrganizationResource | null | undefined; }; @@ -71,6 +82,8 @@ export { OrganizationProvider, useOrganizationContext, UserContext, + OptionsContext, + useOptionsContext, useUserContext, SessionContext, useSessionContext, diff --git a/packages/shared/src/react/index.ts b/packages/shared/src/react/index.ts index 40735d10f2..4b716f4105 100644 --- a/packages/shared/src/react/index.ts +++ b/packages/shared/src/react/index.ts @@ -3,13 +3,15 @@ export * from './hooks'; export { ClerkInstanceContext, ClientContext, + OptionsContext, OrganizationProvider, SessionContext, + useAssertWrappedByClerkProvider, useClerkInstanceContext, useClientContext, + useOptionsContext, useOrganizationContext, UserContext, useSessionContext, useUserContext, - useAssertWrappedByClerkProvider, } from './contexts'; diff --git a/packages/shared/src/router.ts b/packages/shared/src/router.ts index a6bc0ca6a8..32dc0abf2e 100644 --- a/packages/shared/src/router.ts +++ b/packages/shared/src/router.ts @@ -1,3 +1,10 @@ export { type ClerkRouter, type ClerkHostRouter, createClerkRouter } from './router/router'; export { type RoutingMode } from './router/types'; -export { Router, useClerkRouter, Route, ClerkRouterContext } from './router/react'; +export { + Router, + useClerkRouter, + useClerkHostRouter, + Route, + ClerkRouterContext, + ClerkHostRouterContext, +} from './router/react'; diff --git a/packages/shared/src/router/react.tsx b/packages/shared/src/router/react.tsx index c0fcce0649..3fad08e889 100644 --- a/packages/shared/src/router/react.tsx +++ b/packages/shared/src/router/react.tsx @@ -6,8 +6,21 @@ import React, { createContext, useContext } from 'react'; import type { ClerkHostRouter, ClerkRouter } from './router'; import { createClerkRouter } from './router'; +export const ClerkHostRouterContext = createContext(null); export const ClerkRouterContext = createContext(null); +export function useClerkHostRouter() { + const ctx = useContext(ClerkHostRouterContext); + + if (!ctx) { + throw new Error( + 'clerk: Unable to locate ClerkHostRouter, make sure this is rendered within ``.', + ); + } + + return ctx; +} + export function useClerkRouter() { const ctx = useContext(ClerkRouterContext); @@ -28,9 +41,10 @@ export function Router({ }: { children: React.ReactNode; basePath?: string; - router: ClerkHostRouter; + router?: ClerkHostRouter; }) { - const clerkRouter = createClerkRouter(router, basePath); + const hostRouter = useClerkHostRouter(); + const clerkRouter = createClerkRouter(router ?? hostRouter, basePath); return {children}; }