Skip to content

chore(clerk-js,clerk-react,shared): Add hook to assert the presence of ClerkProvider [SDK-1043] #2299

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

Merged
merged 7 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/slow-bugs-exist.md
Original file line number Diff line number Diff line change
@@ -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 `<ClerkProvider />` context an error will be thrown. For example:

```shell
@clerk/clerk-react: useAuth can only be used within the <ClerkProvider /> component
```
14 changes: 14 additions & 0 deletions packages/react/src/components/controlComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>): JSX.Element | null => {
useAssertWrappedByClerkProvider('SignedIn');

const { userId } = useAuthContext();
if (userId) {
return <>{children}</>;
Expand All @@ -23,6 +26,8 @@ export const SignedIn = ({ children }: React.PropsWithChildren<unknown>): JSX.El
};

export const SignedOut = ({ children }: React.PropsWithChildren<unknown>): JSX.Element | null => {
useAssertWrappedByClerkProvider('SignedOut');

const { userId } = useAuthContext();
if (userId === null) {
return <>{children}</>;
Expand All @@ -31,6 +36,8 @@ export const SignedOut = ({ children }: React.PropsWithChildren<unknown>): JSX.E
};

export const ClerkLoaded = ({ children }: React.PropsWithChildren<unknown>): JSX.Element | null => {
useAssertWrappedByClerkProvider('ClerkLoaded');

const isomorphicClerk = useIsomorphicClerkContext();
if (!isomorphicClerk.loaded) {
return null;
Expand All @@ -39,6 +46,8 @@ export const ClerkLoaded = ({ children }: React.PropsWithChildren<unknown>): JSX
};

export const ClerkLoading = ({ children }: React.PropsWithChildren<unknown>): JSX.Element | null => {
useAssertWrappedByClerkProvider('ClerkLoading');

const isomorphicClerk = useIsomorphicClerkContext();
if (isomorphicClerk.loaded) {
return null;
Expand Down Expand Up @@ -86,6 +95,8 @@ type ProtectProps = React.PropsWithChildren<
* ```
*/
export const Protect = ({ children, fallback, ...restAuthorizedParams }: ProtectProps) => {
useAssertWrappedByClerkProvider('Protect');

const { isLoaded, has, userId } = useAuth();

/**
Expand Down Expand Up @@ -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<RedirectToSignInProps>) => {
const { client, session } = clerk;
Expand Down Expand Up @@ -193,6 +205,8 @@ export const AuthenticateWithRedirectCallback = withClerk(
);

export const MultisessionAppSupport = ({ children }: React.PropsWithChildren<unknown>): JSX.Element => {
useAssertWrappedByClerkProvider('MultisessionAppSupport');

const session = useSessionContext();
return <React.Fragment key={session ? session.id : 'no-users'}>{children}</React.Fragment>;
};
3 changes: 3 additions & 0 deletions packages/react/src/components/withClerk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <P extends { clerk: LoadedClerk }>(
Expand All @@ -13,6 +14,8 @@ export const withClerk = <P extends { clerk: LoadedClerk }>(
displayName = displayName || Component.displayName || Component.name || 'Component';
Component.displayName = displayName;
const HOC = (props: Without<P, 'clerk'>) => {
useAssertWrappedByClerkProvider(displayName || 'withClerk');

const clerk = useIsomorphicClerkContext();

if (!clerk.loaded) {
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/hooks/useAssertWrappedByClerkProvider.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
};
3 changes: 3 additions & 0 deletions packages/react/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/hooks/useSignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand All @@ -18,6 +19,8 @@ type UseSignInReturn =
type UseSignIn = () => UseSignInReturn;

export const useSignIn: UseSignIn = () => {
useAssertWrappedByClerkProvider('useSignIn');

const isomorphicClerk = useIsomorphicClerkContext();
const client = useClientContext();

Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/hooks/useSignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand All @@ -18,6 +19,8 @@ type UseSignUpReturn =
type UseSignUp = () => UseSignUpReturn;

export const useSignUp: UseSignUp = () => {
useAssertWrappedByClerkProvider('useSignUp');

const isomorphicClerk = useIsomorphicClerkContext();
const client = useClientContext();

Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ClerkProvider /> component. Learn more: https://clerk.com/docs/components/clerk-provider`,
});

type MessageKeys = keyof typeof DefaultMessages;
Expand All @@ -213,6 +214,9 @@ export interface ErrorThrower {
throwInvalidProxyUrl(params: { url?: string }): never;

throwMissingPublishableKeyError(): never;

throwMissingClerkProviderError(params: { source?: string }): never;

throw(message: string): never;
}

Expand Down Expand Up @@ -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));
},
Expand Down
16 changes: 16 additions & 0 deletions packages/shared/src/react/contexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ClerkProvider /> component. Learn more: https://clerk.com/docs/components/clerk-provider`,
);
}
}

export {
ClientContext,
useClientContext,
Expand All @@ -61,4 +76,5 @@ export {
useSessionContext,
ClerkInstanceContext,
useClerkInstanceContext,
useAssertWrappedByClerkProvider,
};
7 changes: 5 additions & 2 deletions packages/shared/src/react/hooks/useClerk.ts
Original file line number Diff line number Diff line change
@@ -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();
};
10 changes: 9 additions & 1 deletion packages/shared/src/react/hooks/useOrganization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -110,6 +115,9 @@ export const useOrganization: UseOrganization = params => {
memberships: membersListParams,
invitations: invitationsListParams,
} = params || {};

useAssertWrappedByClerkProvider('useOrganization');

const { organization } = useOrganizationContext();
const session = useSessionContext();

Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/react/hooks/useOrganizationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -85,6 +85,8 @@ type UseOrganizationList = <T extends UseOrganizationListParams>(
export const useOrganizationList: UseOrganizationList = params => {
const { userMemberships, userInvitations, userSuggestions } = params || {};

useAssertWrappedByClerkProvider('useOrganizationList');

const userMembershipsSafeValues = useWithSafeValues(userMemberships, {
initialPage: 1,
pageSize: 10,
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/react/hooks/useSession.ts
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down Expand Up @@ -30,6 +30,8 @@ type UseSession = () => UseSessionReturn;
* }
*/
export const useSession: UseSession = () => {
useAssertWrappedByClerkProvider('useSession');

const session = useSessionContext();

if (session === undefined) {
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/react/hooks/useSessionList.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SessionResource, SetActive } from '@clerk/types';

import { useClerkInstanceContext, useClientContext } from '../contexts';
import { useAssertWrappedByClerkProvider, useClerkInstanceContext, useClientContext } from '../contexts';

type UseSessionListReturn =
| {
Expand All @@ -17,6 +17,8 @@ type UseSessionListReturn =
type UseSessionList = () => UseSessionListReturn;

export const useSessionList: UseSessionList = () => {
useAssertWrappedByClerkProvider('useSessionList');

const isomorphicClerk = useClerkInstanceContext();
const client = useClientContext();

Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/react/hooks/useUser.ts
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down Expand Up @@ -28,6 +28,8 @@ type UseUserReturn =
* }
*/
export function useUser(): UseUserReturn {
useAssertWrappedByClerkProvider('useUser');

const user = useUserContext();

if (user === undefined) {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export {
UserContext,
useSessionContext,
useUserContext,
useAssertWrappedByClerkProvider,
} from './contexts';