Skip to content

Commit

Permalink
feat(elements): Remove direct reliance on next and clerk-react (#4064)
Browse files Browse the repository at this point in the history
  • Loading branch information
brkalow authored Sep 4, 2024
1 parent a6a715d commit 95ac67a
Show file tree
Hide file tree
Showing 23 changed files with 149 additions and 72 deletions.
7 changes: 7 additions & 0 deletions .changeset/good-jeans-call.md
Original file line number Diff line number Diff line change
@@ -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`.
2 changes: 0 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions packages/elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
15 changes: 11 additions & 4 deletions packages/elements/src/internals/constants/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const clerkLoader = fromCallback<EventObject, Clerk | LoadedClerk>(({ 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' });
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/react/common/form/input.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/react/common/loading.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/react/hooks/use-password.hook.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
1 change: 0 additions & 1 deletion packages/elements/src/react/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { useNextRouter } from './next';
export { Route, Router, useClerkRouter } from '@clerk/shared/router';
export { useVirtualRouter } from './virtual';
32 changes: 0 additions & 32 deletions packages/elements/src/react/router/next.ts

This file was deleted.

20 changes: 13 additions & 7 deletions packages/elements/src/react/sign-in/root.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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' })) {
Expand Down Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/react/sign-in/step.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
20 changes: 13 additions & 7 deletions packages/elements/src/react/sign-up/root.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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]);

Expand Down Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/react/sign-up/step.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
7 changes: 7 additions & 0 deletions packages/elements/src/utils/safe-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function safeAccess(fn: any, fallback: any) {
try {
return fn();
} catch (e) {
return fallback;
}
}
43 changes: 40 additions & 3 deletions packages/nextjs/src/app-router/client/ClerkProvider.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,12 +19,48 @@ declare global {
__clerk_nav_await: Array<(value: void) => void>;
__clerk_nav: (to: string) => Promise<void>;
__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();
Expand Down Expand Up @@ -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 {
Expand All @@ -84,7 +121,7 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => {
<ClerkNextOptionsProvider options={mergedProps}>
<ReactClerkProvider {...mergedProps}>
<ClerkJSScript router='app' />
{children}
<ClerkHostRouterContext.Provider value={clerkRouter}>{children}</ClerkHostRouterContext.Provider>
</ReactClerkProvider>
</ClerkNextOptionsProvider>
);
Expand Down
13 changes: 13 additions & 0 deletions packages/shared/src/react/contexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type {
ActiveSessionResource,
ClerkOptions,
ClientResource,
LoadedClerk,
OrganizationResource,
Expand All @@ -20,6 +21,16 @@ const [SessionContext, useSessionContext] = createContextAndHook<ActiveSessionRe
'SessionContext',
);

const OptionsContext = React.createContext<ClerkOptions>({});

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;
};
Expand Down Expand Up @@ -71,6 +82,8 @@ export {
OrganizationProvider,
useOrganizationContext,
UserContext,
OptionsContext,
useOptionsContext,
useUserContext,
SessionContext,
useSessionContext,
Expand Down
Loading

0 comments on commit 95ac67a

Please sign in to comment.