diff --git a/.changeset/green-donuts-press.md b/.changeset/green-donuts-press.md new file mode 100644 index 00000000000..69f5e7f50ee --- /dev/null +++ b/.changeset/green-donuts-press.md @@ -0,0 +1,7 @@ +--- +'@clerk/shared': patch +'@clerk/clerk-react': patch +'@clerk/types': patch +--- + +Improve JSDoc documentation diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index 3ac5269302c..893d15d8c32 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -1,5 +1,43 @@ // @ts-check -import { MarkdownRendererEvent } from 'typedoc-plugin-markdown'; +import { MarkdownPageEvent, MarkdownRendererEvent } from 'typedoc-plugin-markdown'; + +/** + * A list of files where we want to remove any headings + */ +const FILES_WITHOUT_HEADINGS = [ + 'use-organization-return.mdx', + 'use-organization-params.mdx', + 'paginated-resources.mdx', + 'pages-or-infinite-options.mdx', + 'pages-or-infinite-options.mdx', + 'paginated-hook-config.mdx', + 'use-organization-list-return.mdx', + 'use-organization-list-params.mdx', +]; + +/** + * An array of tuples where the first element is the file name and the second element is the new path. + */ +const LINK_REPLACEMENTS = [ + ['clerk-paginated-response', '/docs/references/javascript/types/clerk-paginated-response'], + ['paginated-resources', '#paginated-resources'], +]; + +/** + * Inside the generated MDX files are links to other generated MDX files. These relative links need to be replaced with absolute links to pages that exist on clerk.com. + * For example, `[Foobar](../../foo/bar.mdx)` needs to be replaced with `[Foobar](/docs/foo/bar)`. + * It also shouldn't matter how level deep the relative link is. + * + * This function returns an array of `{ pattern: string, replace: string }` to pass into the `typedoc-plugin-replace-text` plugin. + */ +function getRelativeLinkReplacements() { + return LINK_REPLACEMENTS.map(([fileName, newPath]) => { + return { + pattern: new RegExp(`\\((?:\\.{1,2}\\/)+.*?${fileName}\\.mdx\\)`, 'g'), + replace: `(${newPath})`, + }; + }); +} /** * @param {string} str @@ -13,8 +51,9 @@ function toKebabCase(str) { */ export function load(app) { app.renderer.on(MarkdownRendererEvent.BEGIN, output => { - // Do not output README.mdx files + // Modify the output object output.urls = output.urls + // Do not output README.mdx files ?.filter(e => !e.url.endsWith('README.mdx')) .map(e => { // Convert URLs (by default camelCase) to kebab-case @@ -23,7 +62,37 @@ export function load(app) { e.url = kebabUrl; e.model.url = kebabUrl; + /** + * For the `@clerk/shared` package it outputs the hooks as for example: shared/react/hooks/use-clerk/functions/use-clerk.mdx. + * It also places the interfaces as shared/react/hooks/use-organization/interfaces/use-organization-return.mdx + * Group all those .mdx files under shared/react/hooks + */ + if (e.url.includes('shared/react/hooks')) { + e.url = e.url.replace(/\/[^/]+\/(functions|interfaces)\//, '/'); + e.model.url = e.url; + } + return e; }); }); + + app.renderer.on(MarkdownPageEvent.END, output => { + const fileName = output.url.split('/').pop(); + const linkReplacements = getRelativeLinkReplacements(); + + for (const { pattern, replace } of linkReplacements) { + if (output.contents) { + output.contents = output.contents.replace(pattern, replace); + } + } + + if (fileName) { + if (FILES_WITHOUT_HEADINGS.includes(fileName)) { + if (output.contents) { + // Remove any headings from the file, irrespective of the level + output.contents = output.contents.replace(/^#+\s.+/gm, ''); + } + } + } + }); } diff --git a/.typedoc/custom-theme.mjs b/.typedoc/custom-theme.mjs index f721dd79a52..0317bc42086 100644 --- a/.typedoc/custom-theme.mjs +++ b/.typedoc/custom-theme.mjs @@ -1,5 +1,5 @@ // @ts-check -import { ReflectionKind } from 'typedoc'; +import { ArrayType, IntersectionType, ReflectionKind, ReflectionType, UnionType } from 'typedoc'; import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; /** @@ -36,7 +36,7 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { this.partials = { ...superPartials, /** - * Copied from default theme / source code. This hides the return type from the output + * Copied from default theme / source code. This hides the return type heading over the table from the output * https://github.com/typedoc2md/typedoc-plugin-markdown/blob/179a54c502b318cd4f3951e5e8b90f7f7a4752d8/packages/typedoc-plugin-markdown/src/theme/context/partials/member.signatureReturns.ts * @param {import('typedoc').SignatureReflection} model * @param {{ headingLevel: number }} options @@ -235,6 +235,114 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext { md.push(this.partials.body(model, { headingLevel: options.headingLevel })); + return md.join('\n\n'); + }, + /** + * Copied from default theme / source code. This hides the "Type parameters" section and the declaration title from the output + * https://github.com/typedoc2md/typedoc-plugin-markdown/blob/e798507a3c04f9ddf7710baf4cc7836053e438ff/packages/typedoc-plugin-markdown/src/theme/context/partials/member.declaration.ts + * @param {import('typedoc').DeclarationReflection} model + * @param {{ headingLevel: number, nested?: boolean }} options + */ + declaration: (model, options = { headingLevel: 2, nested: false }) => { + const md = []; + + const opts = { + nested: false, + ...options, + }; + + if (!opts.nested && model.sources && !this.options.getValue('disableSources')) { + md.push(this.partials.sources(model)); + } + + if (model?.documents) { + md.push( + this.partials.documents(model, { + headingLevel: options.headingLevel, + }), + ); + } + + /** + * @type any + */ + const modelType = model.type; + /** + * @type {import('typedoc').DeclarationReflection} + */ + let typeDeclaration = modelType?.declaration; + + if (model.type instanceof ArrayType && model.type?.elementType instanceof ReflectionType) { + typeDeclaration = model.type?.elementType?.declaration; + } + + const hasTypeDeclaration = + Boolean(typeDeclaration) || + (model.type instanceof UnionType && model.type?.types.some(type => type instanceof ReflectionType)); + + if (model.comment) { + md.push( + this.partials.comment(model.comment, { + headingLevel: opts.headingLevel, + showSummary: true, + showTags: false, + }), + ); + } + + if (model.type instanceof IntersectionType) { + model.type?.types?.forEach(intersectionType => { + if (intersectionType instanceof ReflectionType && !intersectionType.declaration.signatures) { + if (intersectionType.declaration.children) { + md.push(heading(opts.headingLevel, this.i18n.theme_type_declaration())); + + md.push( + this.partials.typeDeclaration(intersectionType.declaration, { + headingLevel: opts.headingLevel, + }), + ); + } + } + }); + } + + if (hasTypeDeclaration) { + if (model.type instanceof UnionType) { + if (this.helpers.hasUsefulTypeDetails(model.type)) { + md.push(heading(opts.headingLevel, this.i18n.theme_type_declaration())); + + model.type.types.forEach(type => { + if (type instanceof ReflectionType) { + md.push(this.partials.someType(type, { forceCollapse: true })); + md.push(this.partials.typeDeclarationContainer(model, type.declaration, options)); + } else { + md.push(`${this.partials.someType(type)}`); + } + }); + } + } else { + const useHeading = + typeDeclaration?.children?.length && + (model.kind !== ReflectionKind.Property || this.helpers.useTableFormat('properties')); + if (useHeading) { + md.push(heading(opts.headingLevel, this.i18n.theme_type_declaration())); + } + md.push(this.partials.typeDeclarationContainer(model, typeDeclaration, options)); + } + } + if (model.comment) { + md.push( + this.partials.comment(model.comment, { + headingLevel: opts.headingLevel, + showSummary: false, + showTags: true, + showReturns: true, + }), + ); + } + + md.push(this.partials.inheritance(model, { headingLevel: opts.headingLevel })); + return md.join('\n\n'); }, }; diff --git a/.typedoc/typedoc-prettier-config.json b/.typedoc/typedoc-prettier-config.json new file mode 100644 index 00000000000..182fc4bd4b8 --- /dev/null +++ b/.typedoc/typedoc-prettier-config.json @@ -0,0 +1,8 @@ +{ + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "bracketSpacing": true +} diff --git a/package.json b/package.json index 00fe03aaf77..96a58e76513 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "test:integration:tanstack-start": "E2E_APP_ID=tanstack.start pnpm test:integration:base --grep @tanstack-start", "test:integration:vue": "E2E_APP_ID=vue.vite pnpm test:integration:base --grep @vue", "turbo:clean": "turbo daemon clean", - "typedoc:generate": "typedoc --tsconfig tsconfig.typedoc.json", + "typedoc:generate": "pnpm build:declarations && typedoc --tsconfig tsconfig.typedoc.json", "version-packages": "changeset version && pnpm install --lockfile-only --engine-strict=false", "version-packages:canary": "./scripts/canary.mjs", "version-packages:snapshot": "./scripts/snapshot.mjs", diff --git a/packages/react/.gitignore b/packages/react/.gitignore index 5b45b89ce31..7107e6a4038 100644 --- a/packages/react/.gitignore +++ b/packages/react/.gitignore @@ -1,2 +1,3 @@ /*/ !/src/ +!/docs/ diff --git a/packages/react/docs/use-auth.md b/packages/react/docs/use-auth.md new file mode 100644 index 00000000000..18ed8424cd7 --- /dev/null +++ b/packages/react/docs/use-auth.md @@ -0,0 +1,43 @@ + + +```tsx {{ filename: 'app/external-data/page.tsx' }} +'use client'; + +import { useAuth } from '@clerk/nextjs'; + +export default function ExternalDataPage() { + const { userId, sessionId, getToken, isLoaded, isSignedIn } = useAuth(); + + const fetchExternalData = async () => { + const token = await getToken(); + + // Fetch data from an external API + const response = await fetch('https://api.example.com/data', { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + return response.json(); + }; + + if (!isLoaded) { + return
Loading...
; + } + + if (!isSignedIn) { + return
Sign in to view this page
; + } + + return ( +
+

+ Hello, {userId}! Your current active session is {sessionId}. +

+ +
+ ); +} +``` + + diff --git a/packages/react/docs/use-sign-in.md b/packages/react/docs/use-sign-in.md new file mode 100644 index 00000000000..55100d7e212 --- /dev/null +++ b/packages/react/docs/use-sign-in.md @@ -0,0 +1,20 @@ + + +```tsx {{ filename: 'app/sign-in/page.tsx' }} +'use client'; + +import { useSignIn } from '@clerk/nextjs'; + +export default function SignInPage() { + const { isLoaded, signIn } = useSignIn(); + + if (!isLoaded) { + // Handle loading state + return null; + } + + return
The current sign-in attempt status is {signIn?.status}.
; +} +``` + + diff --git a/packages/react/docs/use-sign-up.md b/packages/react/docs/use-sign-up.md new file mode 100644 index 00000000000..53d1cb10289 --- /dev/null +++ b/packages/react/docs/use-sign-up.md @@ -0,0 +1,20 @@ + + +```tsx {{ filename: 'app/sign-up/page.tsx' }} +'use client'; + +import { useSignUp } from '@clerk/nextjs'; + +export default function SignUpPage() { + const { isLoaded, signUp } = useSignUp(); + + if (!isLoaded) { + // Handle loading state + return null; + } + + return
The current sign-up attempt status is {signUp?.status}.
; +} +``` + + diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index a28fee951e2..1d5cca0ecc0 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -21,6 +21,9 @@ import { createGetToken, createSignOut } from './utils'; * * The following example demonstrates how to use the `useAuth()` hook to access the current auth state, like whether the user is signed in or not. It also includes a basic example for using the `getToken()` method to retrieve a session token for fetching data from an external resource. * + * + * + * * ```tsx {{ filename: 'src/pages/ExternalDataPage.tsx' }} * import { useAuth } from '@clerk/clerk-react' * @@ -58,6 +61,14 @@ import { createGetToken, createSignOut } from './utils'; * ) * } * ``` + * + * + * + * + * {@include ../../docs/use-auth.md#nextjs-01} + * + * + * */ export const useAuth = (initialAuthState: any = {}): UseAuthReturn => { useAssertWrappedByClerkProvider('useAuth'); diff --git a/packages/react/src/hooks/useSignIn.ts b/packages/react/src/hooks/useSignIn.ts index dc9bce5ca6b..d9c76fb2115 100644 --- a/packages/react/src/hooks/useSignIn.ts +++ b/packages/react/src/hooks/useSignIn.ts @@ -13,6 +13,9 @@ import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvid * * The following example uses the `useSignIn()` hook to access the [`SignIn`](https://clerk.com/docs/references/javascript/sign-in/sign-in) object, which contains the current sign-in attempt status and methods to create a new sign-in attempt. The `isLoaded` property is used to handle the loading state. * + * + * + * * ```tsx {{ filename: 'src/pages/SignInPage.tsx' }} * import { useSignIn } from '@clerk/clerk-react' * @@ -28,6 +31,14 @@ import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvid * } * ``` * + * + * + * + * {@include ../../docs/use-sign-in.md#nextjs-01} + * + * + * + * * @example * ### Create a custom sign-in flow with `useSignIn()` * diff --git a/packages/react/src/hooks/useSignUp.ts b/packages/react/src/hooks/useSignUp.ts index 4cf9d019d83..b977c38e839 100644 --- a/packages/react/src/hooks/useSignUp.ts +++ b/packages/react/src/hooks/useSignUp.ts @@ -13,6 +13,9 @@ import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvid * * The following example uses the `useSignUp()` hook to access the [`SignUp`](https://clerk.com/docs/references/javascript/sign-up/sign-up) object, which contains the current sign-up attempt status and methods to create a new sign-up attempt. The `isLoaded` property is used to handle the loading state. * + * + * + * * ```tsx {{ filename: 'src/pages/SignUpPage.tsx' }} * import { useSignUp } from '@clerk/clerk-react' * @@ -28,6 +31,14 @@ import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvid * } * ``` * + * + * + * + * {@include ../../docs/use-sign-up.md#nextjs-01} + * + * + * + * * @example * ### Create a custom sign-up flow with `useSignUp()` * diff --git a/packages/shared/.gitignore b/packages/shared/.gitignore index 53005d45b9d..089a307fa51 100644 --- a/packages/shared/.gitignore +++ b/packages/shared/.gitignore @@ -1,3 +1,4 @@ /*/ !/src/ !/scripts/ +!/docs/ diff --git a/packages/shared/docs/use-clerk.md b/packages/shared/docs/use-clerk.md new file mode 100644 index 00000000000..839672b69cf --- /dev/null +++ b/packages/shared/docs/use-clerk.md @@ -0,0 +1,15 @@ + + +```tsx {{ filename: 'app/page.tsx' }} +'use client'; + +import { useClerk } from '@clerk/nextjs'; + +export default function HomePage() { + const clerk = useClerk(); + + return ; +} +``` + + diff --git a/packages/shared/docs/use-session-list.md b/packages/shared/docs/use-session-list.md new file mode 100644 index 00000000000..c4441a59b95 --- /dev/null +++ b/packages/shared/docs/use-session-list.md @@ -0,0 +1,24 @@ + + +```tsx {{ filename: 'app/page.tsx' }} +'use client'; + +import { useSessionList } from '@clerk/nextjs'; + +export default function HomePage() { + const { isLoaded, sessions } = useSessionList(); + + if (!isLoaded) { + // Handle loading state + return null; + } + + return ( +
+

Welcome back. You've been here {sessions.length} times before.

+
+ ); +} +``` + + diff --git a/packages/shared/docs/use-session.md b/packages/shared/docs/use-session.md new file mode 100644 index 00000000000..95be5884665 --- /dev/null +++ b/packages/shared/docs/use-session.md @@ -0,0 +1,28 @@ + + +```tsx {{ filename: 'app/page.tsx' }} +'use client'; + +import { useSession } from '@clerk/nextjs'; + +export default function HomePage() { + const { isLoaded, session, isSignedIn } = useSession(); + + if (!isLoaded) { + // Handle loading state + return null; + } + if (!isSignedIn) { + // Handle signed out state + return null; + } + + return ( +
+

This session has been active since {session.lastActiveAt.toLocaleString()}

+
+ ); +} +``` + + diff --git a/packages/shared/docs/use-user.md b/packages/shared/docs/use-user.md new file mode 100644 index 00000000000..d68f5ebfef4 --- /dev/null +++ b/packages/shared/docs/use-user.md @@ -0,0 +1,76 @@ + + +```tsx {{ filename: 'app/page.tsx' }} +'use client'; + +import { useUser } from '@clerk/nextjs'; + +export default function HomePage() { + const { isLoaded, user } = useUser(); + + if (!isLoaded) { + // Handle loading state + return null; + } + + if (!user) return null; + + const updateUser = async () => { + await user.update({ + firstName: 'John', + lastName: 'Doe', + }); + }; + + return ( + <> + +

user.firstName: {user?.firstName}

+

user.lastName: {user?.lastName}

+ + ); +} +``` + + + + + +```tsx {{ filename: 'app/page.tsx' }} +'use client'; + +import { useUser } from '@clerk/nextjs'; + +export default function HomePage() { + const { isLoaded, user } = useUser(); + + if (!isLoaded) { + // Handle loading state + return null; + } + + if (!user) return null; + + const updateUser = async () => { + // Update data via an API endpoint + const updateMetadata = await fetch('/api/updateMetadata'); + + // Check if the update was successful + if (updateMetadata.message !== 'success') { + throw new Error('Error updating'); + } + + // If the update was successful, reload the user data + await user.reload(); + }; + + return ( + <> + +

user role: {user?.publicMetadata.role}

+ + ); +} +``` + + diff --git a/packages/shared/src/react/hooks/useClerk.ts b/packages/shared/src/react/hooks/useClerk.ts index 931a722aaad..4f993b0eaf2 100644 --- a/packages/shared/src/react/hooks/useClerk.ts +++ b/packages/shared/src/react/hooks/useClerk.ts @@ -14,6 +14,9 @@ import { useAssertWrappedByClerkProvider, useClerkInstanceContext } from '../con * * The following example uses the `useClerk()` hook to access the `clerk` object. The `clerk` object is used to call the [`openSignIn()`](https://clerk.com/docs/references/javascript/clerk#sign-in) method to open the sign-in modal. * + * + * + * * ```tsx {{ filename: 'src/Home.tsx' }} * import { useClerk } from '@clerk/clerk-react' * @@ -23,6 +26,14 @@ import { useAssertWrappedByClerkProvider, useClerkInstanceContext } from '../con * return * } * ``` + * + * + * + * + * {@include ../../../docs/use-clerk.md#nextjs-01} + * + * + * */ export const useClerk = (): LoadedClerk => { useAssertWrappedByClerkProvider('useClerk'); diff --git a/packages/shared/src/react/hooks/useOrganization.tsx b/packages/shared/src/react/hooks/useOrganization.tsx index 25868ab05d7..e8ff9b7ee74 100644 --- a/packages/shared/src/react/hooks/useOrganization.tsx +++ b/packages/shared/src/react/hooks/useOrganization.tsx @@ -22,19 +22,24 @@ import { import type { PaginatedHookConfig, PaginatedResources, PaginatedResourcesWithDefault } from '../types'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; -type UseOrganizationParams = { +/** + * @interface + */ +export type UseOrganizationParams = { /** * If set to `true`, all default properties will be used. * * Otherwise, accepts an object with the following optional properties: * * - `enrollmentMode`: A string that filters the domains by the provided enrollment mode. + * - Any of the properties described in [Shared properties](#shared-properties). */ domains?: true | PaginatedHookConfig; /** * If set to `true`, all default properties will be used. Otherwise, accepts an object with the following optional properties: * * - `status`: A string that filters the membership requests by the provided status. + * - Any of the properties described in [Shared properties](#shared-properties). */ membershipRequests?: true | PaginatedHookConfig; /** @@ -44,6 +49,7 @@ type UseOrganizationParams = { * * - `role`: An array of [`OrganizationCustomRoleKey`](/docs/references/javascript/types/organization-custom-role-key). * - `query`: A string that filters the memberships by the provided string. + * - Any of the properties described in [Shared properties](#shared-properties). */ memberships?: true | PaginatedHookConfig; /** @@ -52,14 +58,15 @@ type UseOrganizationParams = { * Otherwise, accepts an object with the following optional properties: * * - `status`: A string that filters the invitations by the provided status. + * - Any of the properties described in [Shared properties](#shared-properties). */ invitations?: true | PaginatedHookConfig; }; /** - * @inline + * @interface */ -type UseOrganizationReturn = +export type UseOrganizationReturn = | { /** * A boolean that indicates whether Clerk has completed initialization. Initially `false`, becomes `true` once Clerk loads. @@ -121,8 +128,6 @@ type UseOrganizationReturn = > | null; }; -type UseOrganization = (params?: T) => UseOrganizationReturn; - const undefinedPaginatedResource = { data: undefined, count: undefined, @@ -149,7 +154,7 @@ const undefinedPaginatedResource = { * * To keep network usage to a minimum, developers are required to opt-in by specifying which resource they need to fetch and paginate through. By default, the `memberships`, `invitations`, `membershipRequests`, and `domains` attributes are not populated. You must pass `true` or an object with the desired properties to fetch and paginate the data. * - * ```jsx + * ```tsx * // invitations.data will never be populated. * const { invitations } = useOrganization() * @@ -179,7 +184,7 @@ const undefinedPaginatedResource = { * * The following example demonstrates how to use the `infinite` property to fetch and append new data to the existing list. The `memberships` attribute will be populated with the first page of the organization's memberships. When the "Load more" button is clicked, the `fetchNext` helper function will be called to append the next page of memberships to the list. * - * ```jsx + * ```tsx * import { useOrganization } from '@clerk/clerk-react' * * export default function MemberList() { @@ -225,7 +230,7 @@ const undefinedPaginatedResource = { * * Notice the difference between this example's pagination and the infinite pagination example above. * - * ```jsx + * ```tsx * import { useOrganization } from '@clerk/clerk-react' * * export default function MemberList() { @@ -264,7 +269,7 @@ const undefinedPaginatedResource = { * } * ``` */ -export const useOrganization: UseOrganization = params => { +export function useOrganization(params?: T): UseOrganizationReturn { const { domains: domainListParams, membershipRequests: membershipRequestsListParams, @@ -462,4 +467,4 @@ export const useOrganization: UseOrganization = params => { memberships, invitations, }; -}; +} diff --git a/packages/shared/src/react/hooks/useOrganizationList.tsx b/packages/shared/src/react/hooks/useOrganizationList.tsx index 59febf948e8..47d062d411d 100644 --- a/packages/shared/src/react/hooks/useOrganizationList.tsx +++ b/packages/shared/src/react/hooks/useOrganizationList.tsx @@ -16,17 +16,34 @@ import { useAssertWrappedByClerkProvider, useClerkInstanceContext, useUserContex import type { PaginatedHookConfig, PaginatedResources, PaginatedResourcesWithDefault } from '../types'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; -type UseOrganizationListParams = { +/** + * @interface + */ +export type UseOrganizationListParams = { /** - * `true` or an object with any of the properties described in [Shared properties](https://clerk.com/docs/references/react/use-organization-list#shared-properties). If set to `true`, all default properties will be used. + * If set to `true`, all default properties will be used. + * + * Otherwise, accepts an object with the following optional properties: + * + * - Any of the properties described in [Shared properties](#shared-properties). */ userMemberships?: true | PaginatedHookConfig; /** - * `true` or an object with [`status: OrganizationInvitationStatus`](https://clerk.com/docs/references/react/use-organization-list#organization-invitation-status) or any of the properties described in [Shared properties](https://clerk.com/docs/references/react/use-organization-list#shared-properties). If set to `true`, all default properties will be used. + * If set to `true`, all default properties will be used. + * + * Otherwise, accepts an object with the following optional properties: + * + * - `status`: A string that filters the invitations by the provided status. + * - Any of the properties described in [Shared properties](#shared-properties). */ userInvitations?: true | PaginatedHookConfig; /** - * `true` or an object with [`status: OrganizationSuggestionsStatus | OrganizationSuggestionStatus[]`](https://clerk.com/docs/references/react/use-organization-list#organization-suggestion-status) or any of the properties described in [Shared properties](https://clerk.com/docs/references/react/use-organization-list#shared-properties). If set to `true`, all default properties will be used. + * If set to `true`, all default properties will be used. + * + * Otherwise, accepts an object with the following optional properties: + * + * - `status`: A string that filters the suggestions by the provided status. + * - Any of the properties described in [Shared properties](#shared-properties). */ userSuggestions?: true | PaginatedHookConfig; }; @@ -49,9 +66,10 @@ const undefinedPaginatedResource = { setData: undefined, } as const; -type UseOrganizationList = ( - params?: T, -) => +/** + * @interface + */ +export type UseOrganizationListReturn = | { /** * A boolean that indicates whether Clerk has completed initialization. Initially `false`, becomes `true` once Clerk loads. @@ -79,35 +97,17 @@ type UseOrganizationList = ( userSuggestions: PaginatedResourcesWithDefault; } | { - /** - * A boolean that indicates whether Clerk has completed initialization. Initially `false`, becomes `true` once Clerk loads. - */ isLoaded: boolean; - /** - * A function that returns a `Promise` which resolves to the newly created `Organization`. - */ createOrganization: (params: CreateOrganizationParams) => Promise; - /** - * A function that sets the active session and/or organization. - */ setActive: SetActive; - /** - * Returns `PaginatedResources` which includes a list of the user's organization memberships. - */ userMemberships: PaginatedResources< OrganizationMembershipResource, T['userMemberships'] extends { infinite: true } ? true : false >; - /** - * Returns `PaginatedResources` which includes a list of the user's organization invitations. - */ userInvitations: PaginatedResources< UserOrganizationInvitationResource, T['userInvitations'] extends { infinite: true } ? true : false >; - /** - * Returns `PaginatedResources` which includes a list of suggestions for organizations that the user can join. - */ userSuggestions: PaginatedResources< OrganizationSuggestionResource, T['userSuggestions'] extends { infinite: true } ? true : false @@ -116,8 +116,135 @@ type UseOrganizationList = ( /** * The `useOrganizationList()` hook provides access to the current user's organization memberships, invitations, and suggestions. It also includes methods for creating new organizations and managing the active organization. + * + * @example + * ### Expanding and paginating attributes + * + * To keep network usage to a minimum, developers are required to opt-in by specifying which resource they need to fetch and paginate through. So by default, the `userMemberships`, `userInvitations`, and `userSuggestions` attributes are not populated. You must pass true or an object with the desired properties to fetch and paginate the data. + * + * ```tsx + * // userMemberships.data will never be populated + * const { userMemberships } = useOrganizationList() + * + * // Use default values to fetch userMemberships, such as initialPage = 1 and pageSize = 10 + * const { userMemberships } = useOrganizationList({ + * userMemberships: true, + * }) + * + * // Pass your own values to fetch userMemberships + * const { userMemberships } = useOrganizationList({ + * userMemberships: { + * pageSize: 20, + * initialPage: 2, // skips the first page + * }, + * }) + * + * // Aggregate pages in order to render an infinite list + * const { userMemberships } = useOrganizationList({ + * userMemberships: { + * infinite: true, + * }, + * }) + * ``` + * + * @example + * ### Infinite pagination + * + * The following example demonstrates how to use the `infinite` property to fetch and append new data to the existing list. The `userMemberships` attribute will be populated with the first page of the user's organization memberships. When the "Load more" button is clicked, the `fetchNext` helper function will be called to append the next page of memberships to the list. + * + * ```tsx {{ filename: 'src/components/JoinedOrganizations.tsx' }} + * import { useOrganizationList } from '@clerk/clerk-react' + * import React from 'react' + * + * const JoinedOrganizations = () => { + * const { isLoaded, setActive, userMemberships } = useOrganizationList({ + * userMemberships: { + * infinite: true, + * }, + * }) + * + * if (!isLoaded) { + * return <>Loading + * } + * + * return ( + * <> + *
    + * {userMemberships.data?.map((mem) => ( + *
  • + * {mem.organization.name} + * + *
  • + * ))} + *
+ * + * + * + * ) + * } + * + * export default JoinedOrganizations + * ``` + * + * @example + * ### Simple pagination + * + * The following example demonstrates how to use the `fetchPrevious` and `fetchNext` helper functions to paginate through the data. The `userInvitations` attribute will be populated with the first page of invitations. When the "Previous page" or "Next page" button is clicked, the `fetchPrevious` or `fetchNext` helper function will be called to fetch the previous or next page of invitations. + * + * Notice the difference between this example's pagination and the infinite pagination example above. + * + * ```tsx {{ filename: 'src/components/UserInvitationsTable.tsx' }} + * import { useOrganizationList } from '@clerk/clerk-react' + * import React from 'react' + * + * const UserInvitationsTable = () => { + * const { isLoaded, userInvitations } = useOrganizationList({ + * userInvitations: { + * infinite: true, + * keepPreviousData: true, + * }, + * }) + * + * if (!isLoaded || userInvitations.isLoading) { + * return <>Loading + * } + * + * return ( + * <> + * + * + * + * + * + * + * + * + * + * {userInvitations.data?.map((inv) => ( + * + * + * + * + * ))} + * + *
EmailOrg name
{inv.emailAddress}{inv.publicOrganizationData.name}
+ * + * + * + * + * ) + * } + * + * export default UserInvitationsTable + * ``` */ -export const useOrganizationList: UseOrganizationList = params => { +export function useOrganizationList(params?: T): UseOrganizationListReturn { const { userMemberships, userInvitations, userSuggestions } = params || {}; useAssertWrappedByClerkProvider('useOrganizationList'); @@ -253,4 +380,4 @@ export const useOrganizationList: UseOrganizationList = params => { userInvitations: invitations, userSuggestions: suggestions, }; -}; +} diff --git a/packages/shared/src/react/hooks/useSession.ts b/packages/shared/src/react/hooks/useSession.ts index 18eaf012a08..39990ea8898 100644 --- a/packages/shared/src/react/hooks/useSession.ts +++ b/packages/shared/src/react/hooks/useSession.ts @@ -12,6 +12,9 @@ type UseSession = () => UseSessionReturn; * * The following example uses the `useSession()` hook to access the `Session` object, which has the `lastActiveAt` property. The `lastActiveAt` property is a `Date` object used to show the time the session was last active. * + * + * + * * ```tsx {{ filename: 'src/Home.tsx' }} * import { useSession } from '@clerk/clerk-react' * @@ -34,6 +37,14 @@ type UseSession = () => UseSessionReturn; * ) * } * ``` + * + * + * + * + * {@include ../../../docs/use-session.md#nextjs-01} + * + * + * */ export const useSession: UseSession = () => { useAssertWrappedByClerkProvider('useSession'); diff --git a/packages/shared/src/react/hooks/useSessionList.ts b/packages/shared/src/react/hooks/useSessionList.ts index 721492a61d9..e522b918804 100644 --- a/packages/shared/src/react/hooks/useSessionList.ts +++ b/packages/shared/src/react/hooks/useSessionList.ts @@ -10,6 +10,9 @@ import { useAssertWrappedByClerkProvider, useClerkInstanceContext, useClientCont * * The following example uses `useSessionList()` to get a list of sessions that have been registered on the client device. The `sessions` property is used to show the number of times the user has visited the page. * + * + * + * * ```tsx {{ filename: 'src/Home.tsx' }} * import { useSessionList } from '@clerk/clerk-react' * @@ -28,6 +31,14 @@ import { useAssertWrappedByClerkProvider, useClerkInstanceContext, useClientCont * ) * } * ``` + * + * + * + * + * {@include ../../../docs/use-session-list.md#nextjs-01} + * + * + * */ export const useSessionList = (): UseSessionListReturn => { useAssertWrappedByClerkProvider('useSessionList'); diff --git a/packages/shared/src/react/hooks/useUser.ts b/packages/shared/src/react/hooks/useUser.ts index 21ba7ff3585..604cccf4cc1 100644 --- a/packages/shared/src/react/hooks/useUser.ts +++ b/packages/shared/src/react/hooks/useUser.ts @@ -31,7 +31,12 @@ import { useAssertWrappedByClerkProvider, useUserContext } from '../contexts'; * * The following example uses the `useUser()` hook to access the [`User`](https://clerk.com/docs/references/javascript/user) object, which calls the [`update()`](https://clerk.com/docs/references/javascript/user#update) method to update the current user's information. * + * + * + * * ```tsx {{ filename: 'src/Home.tsx' }} + * import { useUser } from '@clerk/clerk-react' + * * export default function Home() { * const { isLoaded, user } = useUser() * @@ -58,13 +63,25 @@ import { useAssertWrappedByClerkProvider, useUserContext } from '../contexts'; * ) * } * ``` + * + * + * + * {@include ../../../docs/use-user.md#nextjs-01} + * + * + * * * @example * ### Reload user data * * The following example uses the `useUser()` hook to access the [`User`](https://clerk.com/docs/references/javascript/user) object, which calls the [`reload()`](https://clerk.com/docs/references/javascript/user#reload) method to get the latest user's information. * + * + * + * * ```tsx {{ filename: 'src/Home.tsx' }} + * import { useUser } from '@clerk/clerk-react' + * * export default function Home() { * const { isLoaded, user } = useUser() * @@ -96,6 +113,14 @@ import { useAssertWrappedByClerkProvider, useUserContext } from '../contexts'; * ) * } * ``` + * + * + * + * + * {@include ../../../docs/use-user.md#nextjs-02} + * + * + * */ export function useUser(): UseUserReturn { useAssertWrappedByClerkProvider('useUser'); diff --git a/packages/shared/src/react/types.ts b/packages/shared/src/react/types.ts index 7697d8dc5e0..2dfcf01d7d8 100644 --- a/packages/shared/src/react/types.ts +++ b/packages/shared/src/react/types.ts @@ -85,7 +85,7 @@ export type PaginatedResourcesWithDefault = { }; /** - * @interface + * @inline */ export type PaginatedHookConfig = T & { /** diff --git a/packages/shared/typedoc.json b/packages/shared/typedoc.json index 5a7f962a961..332cb7df315 100644 --- a/packages/shared/typedoc.json +++ b/packages/shared/typedoc.json @@ -1,6 +1,6 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["./src/index.ts", "./src/react/index.ts", "./src/react/types.ts"], + "entryPoints": ["./src/index.ts", "./src/react/types.ts", "./src/react/hooks/*.{ts,tsx}"], "compilerOptions": { "noImplicitAny": false } diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index e1366319454..2127800578f 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -81,6 +81,9 @@ export type SignOutOptions = { redirectUrl?: string; }; +/** + * @inline + */ export interface SignOut { (options?: SignOutOptions): Promise; @@ -1529,6 +1532,9 @@ export type CreateBulkOrganizationInvitationParams = { role: OrganizationCustomRoleKey; }; +/** + * @inline + */ export interface CreateOrganizationParams { name: string; slug?: string; diff --git a/packages/types/src/pagination.ts b/packages/types/src/pagination.ts index 40f5388a0c9..3925a6f2cd0 100644 --- a/packages/types/src/pagination.ts +++ b/packages/types/src/pagination.ts @@ -13,23 +13,32 @@ export type ClerkPaginationRequest = { } & T; /** - * Pagination params in response + * An interface that describes the response of a method that returns a paginated list of resources. + * + * > [!TIP] + * > Clerk's SDKs always use `Promise>`. If the promise resolves, you will get back the properties. If the promise is rejected, you will receive a `ClerkAPIResponseError` or network error. */ export interface ClerkPaginatedResponse { + /** + * An array that contains the fetched data. + */ data: T[]; + /** + * The total count of data that exist remotely. + */ total_count: number; } /** - * Pagination params passed in FAPI client methods + * @interface */ export type ClerkPaginationParams = { /** - * This is the starting point for your fetched results. + * A number that specifies which page to fetch. For example, if `initialPage` is set to `10`, it will skip the first 9 pages and fetch the 10th page. Defaults to `1`. */ initialPage?: number; /** - * Maximum number of items returned per request. + * A number that specifies the maximum number of results to return per page. Defaults to `10`. */ pageSize?: number; } & T; diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 44aaa87f4d6..5c93a6b630f 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -33,6 +33,7 @@ const typedocPluginMarkdownOptions = { fileExtension: '.mdx', excludeScopesInPaths: true, expandObjects: true, + formatWithPrettier: true, }; /** @type {Partial} */ @@ -55,6 +56,17 @@ const typedocPluginReplaceTextOptions = { pattern: /```empty```/, replace: '', }, + { + /** + * In order to not render `` in the inline IntelliSense, the `items` prop was adjusted from `items={['item', 'item2']}` to `items='item,item2'`. It needs to be converted back so that clerk.com can render it properly. + */ + pattern: /Tabs items='([^']+)'/, + replace: (_, match) => + `Tabs items={[${match + .split(',') + .map(item => `'${item.trim()}'`) + .join(', ')}]}`, + }, ], }, };