diff --git a/.changeset/slow-bugs-exist.md b/.changeset/slow-bugs-exist.md new file mode 100644 index 00000000000..f9e4d57bd45 --- /dev/null +++ b/.changeset/slow-bugs-exist.md @@ -0,0 +1,11 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/clerk-react': patch +--- + +Add `useAssertWrappedByClerkProvider` to internal code. If you use hooks like `useAuth` outside of the `` context an error will be thrown. For example: + +```shell +@clerk/clerk-react: useAuth can only be used within the component +``` diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 4b05c0cfd31..7a0c5ccfe1d 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -11,10 +11,13 @@ import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; import { useSessionContext } from '../contexts/SessionContext'; import { LoadedGuarantee } from '../contexts/StructureContext'; import { useAuth } from '../hooks'; +import { useAssertWrappedByClerkProvider } from '../hooks/useAssertWrappedByClerkProvider'; import type { RedirectToSignInProps, RedirectToSignUpProps, WithClerkProp } from '../types'; import { withClerk } from './withClerk'; export const SignedIn = ({ children }: React.PropsWithChildren): JSX.Element | null => { + useAssertWrappedByClerkProvider('SignedIn'); + const { userId } = useAuthContext(); if (userId) { return <>{children}; @@ -23,6 +26,8 @@ export const SignedIn = ({ children }: React.PropsWithChildren): JSX.El }; export const SignedOut = ({ children }: React.PropsWithChildren): JSX.Element | null => { + useAssertWrappedByClerkProvider('SignedOut'); + const { userId } = useAuthContext(); if (userId === null) { return <>{children}; @@ -31,6 +36,8 @@ export const SignedOut = ({ children }: React.PropsWithChildren): JSX.E }; export const ClerkLoaded = ({ children }: React.PropsWithChildren): JSX.Element | null => { + useAssertWrappedByClerkProvider('ClerkLoaded'); + const isomorphicClerk = useIsomorphicClerkContext(); if (!isomorphicClerk.loaded) { return null; @@ -39,6 +46,8 @@ export const ClerkLoaded = ({ children }: React.PropsWithChildren): JSX }; export const ClerkLoading = ({ children }: React.PropsWithChildren): JSX.Element | null => { + useAssertWrappedByClerkProvider('ClerkLoading'); + const isomorphicClerk = useIsomorphicClerkContext(); if (isomorphicClerk.loaded) { return null; @@ -86,6 +95,8 @@ type ProtectProps = React.PropsWithChildren< * ``` */ export const Protect = ({ children, fallback, ...restAuthorizedParams }: ProtectProps) => { + useAssertWrappedByClerkProvider('Protect'); + const { isLoaded, has, userId } = useAuth(); /** @@ -129,6 +140,7 @@ export const Protect = ({ children, fallback, ...restAuthorizedParams }: Protect */ return authorized; }; +/* eslint-enable react-hooks/rules-of-hooks */ export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => { const { client, session } = clerk; @@ -193,6 +205,8 @@ export const AuthenticateWithRedirectCallback = withClerk( ); export const MultisessionAppSupport = ({ children }: React.PropsWithChildren): JSX.Element => { + useAssertWrappedByClerkProvider('MultisessionAppSupport'); + const session = useSessionContext(); return {children}; }; diff --git a/packages/react/src/components/withClerk.tsx b/packages/react/src/components/withClerk.tsx index 1048b154d82..c8f0a0f5668 100644 --- a/packages/react/src/components/withClerk.tsx +++ b/packages/react/src/components/withClerk.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; import { LoadedGuarantee } from '../contexts/StructureContext'; import { hocChildrenNotAFunctionError } from '../errors'; +import { useAssertWrappedByClerkProvider } from '../hooks/useAssertWrappedByClerkProvider'; import { errorThrower } from '../utils'; export const withClerk =

( @@ -13,6 +14,8 @@ export const withClerk =

( displayName = displayName || Component.displayName || Component.name || 'Component'; Component.displayName = displayName; const HOC = (props: Without) => { + useAssertWrappedByClerkProvider(displayName || 'withClerk'); + const clerk = useIsomorphicClerkContext(); if (!clerk.loaded) { diff --git a/packages/react/src/hooks/useAssertWrappedByClerkProvider.ts b/packages/react/src/hooks/useAssertWrappedByClerkProvider.ts new file mode 100644 index 00000000000..1fad233ec66 --- /dev/null +++ b/packages/react/src/hooks/useAssertWrappedByClerkProvider.ts @@ -0,0 +1,9 @@ +import { useAssertWrappedByClerkProvider as useSharedAssertWrappedByClerkProvider } from '@clerk/shared/react'; + +import { errorThrower } from '../utils'; + +export const useAssertWrappedByClerkProvider = (source: string): void => { + useSharedAssertWrappedByClerkProvider(() => { + errorThrower.throwMissingClerkProviderError({ source }); + }); +}; diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index b8e60b3d1e6..661d69b5778 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -11,6 +11,7 @@ import { useAuthContext } from '../contexts/AuthContext'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; import { invalidStateError, useAuthHasRequiresRoleOrPermission } from '../errors'; import { errorThrower } from '../utils'; +import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider'; import { createGetToken, createSignOut } from './utils'; type CheckAuthorizationSignedOut = undefined; @@ -109,6 +110,8 @@ type UseAuth = () => UseAuthReturn; * } */ export const useAuth: UseAuth = () => { + useAssertWrappedByClerkProvider('useAuth'); + const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useAuthContext(); const isomorphicClerk = useIsomorphicClerkContext(); diff --git a/packages/react/src/hooks/useSignIn.ts b/packages/react/src/hooks/useSignIn.ts index 73885d47cc5..d666940dd84 100644 --- a/packages/react/src/hooks/useSignIn.ts +++ b/packages/react/src/hooks/useSignIn.ts @@ -2,6 +2,7 @@ import { useClientContext } from '@clerk/shared/react'; import type { SetActive, SignInResource } from '@clerk/types'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; +import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider'; type UseSignInReturn = | { @@ -18,6 +19,8 @@ type UseSignInReturn = type UseSignIn = () => UseSignInReturn; export const useSignIn: UseSignIn = () => { + useAssertWrappedByClerkProvider('useSignIn'); + const isomorphicClerk = useIsomorphicClerkContext(); const client = useClientContext(); diff --git a/packages/react/src/hooks/useSignUp.ts b/packages/react/src/hooks/useSignUp.ts index 1398773b2a9..5b169c2779a 100644 --- a/packages/react/src/hooks/useSignUp.ts +++ b/packages/react/src/hooks/useSignUp.ts @@ -2,6 +2,7 @@ import { useClientContext } from '@clerk/shared/react'; import type { SetActive, SignUpResource } from '@clerk/types'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; +import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider'; type UseSignUpReturn = | { @@ -18,6 +19,8 @@ type UseSignUpReturn = type UseSignUp = () => UseSignUpReturn; export const useSignUp: UseSignUp = () => { + useAssertWrappedByClerkProvider('useSignUp'); + const isomorphicClerk = useIsomorphicClerkContext(); const client = useClientContext(); diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 8f35f232a63..6cf10ef7f6e 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -190,6 +190,7 @@ const DefaultMessages = Object.freeze({ InvalidProxyUrlErrorMessage: `The proxyUrl passed to Clerk is invalid. The expected value for proxyUrl is an absolute URL or a relative path with a leading '/'. (key={{url}})`, InvalidPublishableKeyErrorMessage: `The publishableKey passed to Clerk is invalid. You can get your Publishable key at https://dashboard.clerk.com/last-active?path=api-keys. (key={{key}})`, MissingPublishableKeyErrorMessage: `Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, + MissingClerkProvider: `{{source}} can only be used within the component. Learn more: https://clerk.com/docs/components/clerk-provider`, }); type MessageKeys = keyof typeof DefaultMessages; @@ -213,6 +214,9 @@ export interface ErrorThrower { throwInvalidProxyUrl(params: { url?: string }): never; throwMissingPublishableKeyError(): never; + + throwMissingClerkProviderError(params: { source?: string }): never; + throw(message: string): never; } @@ -265,6 +269,10 @@ export function buildErrorThrower({ packageName, customMessages }: ErrorThrowerO throw new Error(buildMessage(messages.MissingPublishableKeyErrorMessage)); }, + throwMissingClerkProviderError(params: { source?: string }): never { + throw new Error(buildMessage(messages.MissingClerkProvider, params)); + }, + throw(message: string): never { throw new Error(buildMessage(message)); }, diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index de372140a89..51b9bab76db 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -50,6 +50,21 @@ const OrganizationProvider = ({ ); }; +function useAssertWrappedByClerkProvider(displayNameOrFn: string | (() => void)): void { + const ctx = React.useContext(ClerkInstanceContext); + + if (!ctx) { + if (typeof displayNameOrFn === 'function') { + displayNameOrFn(); + return; + } + + throw new Error( + `${displayNameOrFn} can only be used within the component. Learn more: https://clerk.com/docs/components/clerk-provider`, + ); + } +} + export { ClientContext, useClientContext, @@ -61,4 +76,5 @@ export { useSessionContext, ClerkInstanceContext, useClerkInstanceContext, + useAssertWrappedByClerkProvider, }; diff --git a/packages/shared/src/react/hooks/useClerk.ts b/packages/shared/src/react/hooks/useClerk.ts index 505cab5e80a..c65bd45a0e9 100644 --- a/packages/shared/src/react/hooks/useClerk.ts +++ b/packages/shared/src/react/hooks/useClerk.ts @@ -1,5 +1,8 @@ import type { LoadedClerk } from '@clerk/types'; -import { useClerkInstanceContext } from '../contexts'; +import { useAssertWrappedByClerkProvider, useClerkInstanceContext } from '../contexts'; -export const useClerk: () => LoadedClerk = useClerkInstanceContext; +export const useClerk = (): LoadedClerk => { + useAssertWrappedByClerkProvider('useClerk'); + return useClerkInstanceContext(); +}; diff --git a/packages/shared/src/react/hooks/useOrganization.tsx b/packages/shared/src/react/hooks/useOrganization.tsx index 483fb158458..044f1461859 100644 --- a/packages/shared/src/react/hooks/useOrganization.tsx +++ b/packages/shared/src/react/hooks/useOrganization.tsx @@ -11,7 +11,12 @@ import type { OrganizationResource, } from '@clerk/types'; -import { useClerkInstanceContext, useOrganizationContext, useSessionContext } from '../contexts'; +import { + useAssertWrappedByClerkProvider, + useClerkInstanceContext, + useOrganizationContext, + useSessionContext, +} from '../contexts'; import type { PaginatedResources, PaginatedResourcesWithDefault } from '../types'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; @@ -110,6 +115,9 @@ export const useOrganization: UseOrganization = params => { memberships: membersListParams, invitations: invitationsListParams, } = params || {}; + + useAssertWrappedByClerkProvider('useOrganization'); + const { organization } = useOrganizationContext(); const session = useSessionContext(); diff --git a/packages/shared/src/react/hooks/useOrganizationList.tsx b/packages/shared/src/react/hooks/useOrganizationList.tsx index 80dc52e0af4..2f879a92d38 100644 --- a/packages/shared/src/react/hooks/useOrganizationList.tsx +++ b/packages/shared/src/react/hooks/useOrganizationList.tsx @@ -11,7 +11,7 @@ import type { UserOrganizationInvitationResource, } from '@clerk/types'; -import { useClerkInstanceContext, useUserContext } from '../contexts'; +import { useAssertWrappedByClerkProvider, useClerkInstanceContext, useUserContext } from '../contexts'; import type { PaginatedResources, PaginatedResourcesWithDefault } from '../types'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; @@ -85,6 +85,8 @@ type UseOrganizationList = ( export const useOrganizationList: UseOrganizationList = params => { const { userMemberships, userInvitations, userSuggestions } = params || {}; + useAssertWrappedByClerkProvider('useOrganizationList'); + const userMembershipsSafeValues = useWithSafeValues(userMemberships, { initialPage: 1, pageSize: 10, diff --git a/packages/shared/src/react/hooks/useSession.ts b/packages/shared/src/react/hooks/useSession.ts index db27bbbaf34..6f9544483ec 100644 --- a/packages/shared/src/react/hooks/useSession.ts +++ b/packages/shared/src/react/hooks/useSession.ts @@ -1,6 +1,6 @@ import type { ActiveSessionResource } from '@clerk/types'; -import { useSessionContext } from '../contexts'; +import { useAssertWrappedByClerkProvider, useSessionContext } from '../contexts'; type UseSessionReturn = | { isLoaded: false; isSignedIn: undefined; session: undefined } @@ -30,6 +30,8 @@ type UseSession = () => UseSessionReturn; * } */ export const useSession: UseSession = () => { + useAssertWrappedByClerkProvider('useSession'); + const session = useSessionContext(); if (session === undefined) { diff --git a/packages/shared/src/react/hooks/useSessionList.ts b/packages/shared/src/react/hooks/useSessionList.ts index 03f2cdcdbf4..32e9c3718d7 100644 --- a/packages/shared/src/react/hooks/useSessionList.ts +++ b/packages/shared/src/react/hooks/useSessionList.ts @@ -1,6 +1,6 @@ import type { SessionResource, SetActive } from '@clerk/types'; -import { useClerkInstanceContext, useClientContext } from '../contexts'; +import { useAssertWrappedByClerkProvider, useClerkInstanceContext, useClientContext } from '../contexts'; type UseSessionListReturn = | { @@ -17,6 +17,8 @@ type UseSessionListReturn = type UseSessionList = () => UseSessionListReturn; export const useSessionList: UseSessionList = () => { + useAssertWrappedByClerkProvider('useSessionList'); + const isomorphicClerk = useClerkInstanceContext(); const client = useClientContext(); diff --git a/packages/shared/src/react/hooks/useUser.ts b/packages/shared/src/react/hooks/useUser.ts index 89a0934de1b..a1bc3686738 100644 --- a/packages/shared/src/react/hooks/useUser.ts +++ b/packages/shared/src/react/hooks/useUser.ts @@ -1,6 +1,6 @@ import type { UserResource } from '@clerk/types'; -import { useUserContext } from '../contexts'; +import { useAssertWrappedByClerkProvider, useUserContext } from '../contexts'; type UseUserReturn = | { isLoaded: false; isSignedIn: undefined; user: undefined } @@ -28,6 +28,8 @@ type UseUserReturn = * } */ export function useUser(): UseUserReturn { + useAssertWrappedByClerkProvider('useUser'); + const user = useUserContext(); if (user === undefined) { diff --git a/packages/shared/src/react/index.ts b/packages/shared/src/react/index.ts index 1a4c5968064..40735d10f2d 100644 --- a/packages/shared/src/react/index.ts +++ b/packages/shared/src/react/index.ts @@ -11,4 +11,5 @@ export { UserContext, useSessionContext, useUserContext, + useAssertWrappedByClerkProvider, } from './contexts';