diff --git a/.changeset/thirty-pumas-hope.md b/.changeset/thirty-pumas-hope.md new file mode 100644 index 00000000000..a9a7aa90d3c --- /dev/null +++ b/.changeset/thirty-pumas-hope.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': patch +--- + +Avoid revalidating first page on infinite pagination. diff --git a/packages/clerk-js/src/test/create-fixtures.tsx b/packages/clerk-js/src/test/create-fixtures.tsx index 9b5168055bd..e9af007e3aa 100644 --- a/packages/clerk-js/src/test/create-fixtures.tsx +++ b/packages/clerk-js/src/test/create-fixtures.tsx @@ -1,4 +1,5 @@ import type { ClerkOptions, ClientJSON, EnvironmentJSON, LoadedClerk } from '@clerk/shared/types'; +import { useState } from 'react'; import { vi } from 'vitest'; import { Clerk as ClerkCtor } from '@/core/clerk'; @@ -83,6 +84,7 @@ const unboundCreateFixtures = ( const MockClerkProvider = (props: any) => { const { children } = props; + const [swrConfig] = useState(() => ({ provider: () => new Map() })); const componentsWithoutContext = [ 'UsernameSection', @@ -106,7 +108,7 @@ const unboundCreateFixtures = ( new Map() }} + swrConfig={swrConfig} > diff --git a/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganization.test.tsx b/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganization.test.tsx index 7dd82f953b8..712de8d701f 100644 --- a/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganization.test.tsx +++ b/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganization.test.tsx @@ -1,4 +1,5 @@ import { useOrganization } from '@clerk/shared/react'; +import { createDeferredPromise } from '@clerk/shared/utils/index'; import { describe, expect, it } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; @@ -42,7 +43,7 @@ const undefinedPaginatedResource = { describe('useOrganization', () => { it('returns default values', async () => { - const { wrapper } = await createFixtures(f => { + const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.com'], @@ -65,10 +66,15 @@ describe('useOrganization', () => { expect(result.current.memberships).toEqual(expect.objectContaining(undefinedPaginatedResource)); expect(result.current.domains).toEqual(expect.objectContaining(undefinedPaginatedResource)); expect(result.current.membershipRequests).toEqual(expect.objectContaining(undefinedPaginatedResource)); + + expect(fixtures.clerk.organization?.getMemberships).not.toHaveBeenCalled(); + expect(fixtures.clerk.organization?.getDomains).not.toHaveBeenCalled(); + expect(fixtures.clerk.organization?.getMembershipRequests).not.toHaveBeenCalled(); + expect(fixtures.clerk.organization?.getInvitations).not.toHaveBeenCalled(); }); - it('returns null when a organization is not active ', async () => { - const { wrapper } = await createFixtures(f => { + it('returns null when a organization is not active', async () => { + const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.com'], @@ -83,6 +89,8 @@ describe('useOrganization', () => { expect(result.current.memberships).toBeNull(); expect(result.current.domains).toBeNull(); expect(result.current.membershipRequests).toBeNull(); + + expect(fixtures.clerk.organization).toBeNull(); }); describe('memberships', () => { @@ -543,52 +551,34 @@ describe('useOrganization', () => { await waitFor(() => expect(result.current.invitations?.isLoading).toBe(false)); expect(result.current.invitations?.isFetching).toBe(false); - fixtures.clerk.organization?.getInvitations.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeOrganizationInvitation({ - id: '1', - emailAddress: 'admin1@clerk.com', - organizationId: '1', - createdAt: new Date('2022-01-01'), - }), - createFakeOrganizationInvitation({ - id: '2', - emailAddress: 'member2@clerk.com', - organizationId: '1', - createdAt: new Date('2022-01-01'), - }), - ], - total_count: 4, - }), - ); - - fixtures.clerk.organization?.getInvitations.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeOrganizationInvitation({ - id: '3', - emailAddress: 'admin3@clerk.com', - organizationId: '1', - createdAt: new Date('2022-01-01'), - }), - createFakeOrganizationInvitation({ - id: '4', - emailAddress: 'member4@clerk.com', - organizationId: '1', - createdAt: new Date('2022-01-01'), - }), - ], - total_count: 4, - }), - ); + const deferred = createDeferredPromise(); + fixtures.clerk.organization?.getInvitations.mockReturnValueOnce(deferred.promise); act(() => result.current.invitations?.fetchNext?.()); await waitFor(() => expect(result.current.invitations?.isFetching).toBe(true)); expect(result.current.invitations?.isLoading).toBe(false); + deferred.resolve({ + data: [ + createFakeOrganizationInvitation({ + id: '3', + emailAddress: 'admin3@clerk.com', + organizationId: '1', + createdAt: new Date('2022-01-01'), + }), + createFakeOrganizationInvitation({ + id: '4', + emailAddress: 'member4@clerk.com', + organizationId: '1', + createdAt: new Date('2022-01-01'), + }), + ], + total_count: 4, + }); + await waitFor(() => expect(result.current.invitations?.isFetching).toBe(false)); + expect(result.current.invitations?.data).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx b/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx index aa912e5572e..d31d85c9cc0 100644 --- a/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx +++ b/packages/clerk-js/src/ui/hooks/__tests__/useCoreOrganizationList.test.tsx @@ -1,4 +1,5 @@ import { useOrganizationList } from '@clerk/shared/react'; +import { createDeferredPromise } from '@clerk/shared/utils/index'; import { describe, expect, it } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; @@ -65,6 +66,9 @@ describe('useOrganizationList', () => { }); }); + fixtures.clerk.user?.getOrganizationInvitations.mockRejectedValue(null); + fixtures.clerk.user?.getOrganizationSuggestions.mockRejectedValue(null); + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValue( Promise.resolve({ data: [ @@ -102,6 +106,8 @@ describe('useOrganizationList', () => { expect(result.current.userMemberships.count).toBe(0); await waitFor(() => expect(result.current.userMemberships.isLoading).toBe(false)); + await waitFor(() => expect(fixtures.clerk.user?.getOrganizationInvitations).toHaveBeenCalled()); + await waitFor(() => expect(fixtures.clerk.user?.getOrganizationSuggestions).toHaveBeenCalled()); expect(result.current.userMemberships.count).toBe(4); expect(result.current.userMemberships.page).toBe(1); @@ -174,7 +180,7 @@ describe('useOrganizationList', () => { }); }); - fixtures.clerk.user?.getOrganizationMemberships.mockReturnValue( + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( Promise.resolve({ data: [ createFakeUserOrganizationMembership({ @@ -221,75 +227,44 @@ describe('useOrganizationList', () => { await waitFor(() => expect(result.current.userMemberships.isLoading).toBe(false)); expect(result.current.userMemberships.isFetching).toBe(false); - fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeUserOrganizationMembership({ - id: '1', - organization: { - id: '1', - name: 'Org1', - slug: 'org1', - membersCount: 1, - adminDeleteEnabled: false, - maxAllowedMemberships: 0, - pendingInvitationsCount: 1, - }, - }), - createFakeUserOrganizationMembership({ - id: '2', - organization: { - id: '2', - name: 'Org2', - slug: 'org2', - membersCount: 1, - adminDeleteEnabled: false, - maxAllowedMemberships: 0, - pendingInvitationsCount: 1, - }, - }), - ], - total_count: 4, - }), - ); - - fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeUserOrganizationMembership({ - id: '3', - organization: { - id: '3', - name: 'Org3', - slug: 'org3', - membersCount: 1, - adminDeleteEnabled: false, - maxAllowedMemberships: 0, - pendingInvitationsCount: 1, - }, - }), - createFakeUserOrganizationMembership({ - id: '4', - organization: { - id: '4', - name: 'Org4', - slug: 'org4', - membersCount: 1, - adminDeleteEnabled: false, - maxAllowedMemberships: 0, - pendingInvitationsCount: 1, - }, - }), - ], - total_count: 4, - }), - ); + const deferred = createDeferredPromise(); + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce(deferred.promise); act(() => result.current.userMemberships?.fetchNext?.()); await waitFor(() => expect(result.current.userMemberships?.isFetching).toBe(true)); expect(result.current.userMemberships?.isLoading).toBe(false); + deferred.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '3', + organization: { + id: '3', + name: 'Org3', + slug: 'org3', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 0, + pendingInvitationsCount: 1, + }, + }), + createFakeUserOrganizationMembership({ + id: '4', + organization: { + id: '4', + name: 'Org4', + slug: 'org4', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 0, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 4, + }); + await waitFor(() => expect(result.current.userMemberships?.isFetching).toBe(false)); expect(result.current.userMemberships.data).toEqual( expect.arrayContaining([ @@ -399,7 +374,7 @@ describe('useOrganizationList', () => { }); }); - fixtures.clerk.user?.getOrganizationInvitations.mockReturnValue( + fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce( Promise.resolve({ data: [ createFakeUserOrganizationInvitation({ @@ -431,43 +406,28 @@ describe('useOrganizationList', () => { await waitFor(() => expect(result.current.userInvitations.isLoading).toBe(false)); expect(result.current.userInvitations.isFetching).toBe(false); - fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeUserOrganizationInvitation({ - id: '1', - emailAddress: 'one@clerk.com', - }), - createFakeUserOrganizationInvitation({ - id: '2', - emailAddress: 'two@clerk.com', - }), - ], - total_count: 4, - }), - ); - - fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeUserOrganizationInvitation({ - id: '3', - emailAddress: 'three@clerk.com', - }), - createFakeUserOrganizationInvitation({ - id: '4', - emailAddress: 'four@clerk.com', - }), - ], - total_count: 4, - }), - ); + const deferred = createDeferredPromise(); + fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(deferred.promise); act(() => result.current.userInvitations.fetchNext?.()); await waitFor(() => expect(result.current.userInvitations.isFetching).toBe(true)); expect(result.current.userInvitations.isLoading).toBe(false); + deferred.resolve({ + data: [ + createFakeUserOrganizationInvitation({ + id: '3', + emailAddress: 'three@clerk.com', + }), + createFakeUserOrganizationInvitation({ + id: '4', + emailAddress: 'four@clerk.com', + }), + ], + total_count: 4, + }); + await waitFor(() => expect(result.current.userInvitations.isFetching).toBe(false)); expect(result.current.userInvitations.data).toEqual( expect.arrayContaining([ @@ -575,7 +535,7 @@ describe('useOrganizationList', () => { }); }); - fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValue( + fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce( Promise.resolve({ data: [ createFakeUserOrganizationSuggestion({ @@ -606,43 +566,28 @@ describe('useOrganizationList', () => { await waitFor(() => expect(result.current.userSuggestions.isLoading).toBe(false)); expect(result.current.userSuggestions.isFetching).toBe(false); - fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeUserOrganizationSuggestion({ - id: '1', - emailAddress: 'one@clerk.com', - }), - createFakeUserOrganizationSuggestion({ - id: '2', - emailAddress: 'two@clerk.com', - }), - ], - total_count: 4, - }), - ); - - fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeUserOrganizationSuggestion({ - id: '3', - emailAddress: 'three@clerk.com', - }), - createFakeUserOrganizationSuggestion({ - id: '4', - emailAddress: 'four@clerk.com', - }), - ], - total_count: 4, - }), - ); + const deferred = createDeferredPromise(); + fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce(deferred.promise); act(() => result.current.userSuggestions.fetchNext?.()); await waitFor(() => expect(result.current.userSuggestions.isFetching).toBe(true)); expect(result.current.userSuggestions.isLoading).toBe(false); + deferred.resolve({ + data: [ + createFakeUserOrganizationSuggestion({ + id: '3', + emailAddress: 'three@clerk.com', + }), + createFakeUserOrganizationSuggestion({ + id: '4', + emailAddress: 'four@clerk.com', + }), + ], + total_count: 4, + }); + await waitFor(() => expect(result.current.userSuggestions.isFetching).toBe(false)); expect(result.current.userSuggestions.data).toEqual( expect.arrayContaining([ diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.ts b/packages/shared/src/react/hooks/usePagesOrInfinite.ts index b5d0c5aed45..45abe5317ff 100644 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.ts +++ b/packages/shared/src/react/hooks/usePagesOrInfinite.ts @@ -98,6 +98,11 @@ const cachingSWROptions = { focusThrottleInterval: 1000 * 60 * 2, } satisfies Parameters[2]; +const cachingSWRInfiniteOptions = { + ...cachingSWROptions, + revalidateFirstPage: false, +} satisfies Parameters[2]; + type ArrayType = DataArray extends Array ? ElementType : never; type ExtractData = Type extends { data: infer Data } ? ArrayType : Type; @@ -249,7 +254,7 @@ export const usePagesOrInfinite: UsePagesOrInfinite = (params, fetcher, config, // @ts-ignore - fetcher expects Params subset; narrowing at call-site return fetcher?.(requestParams); }, - cachingSWROptions, + cachingSWRInfiniteOptions, ); const page = useMemo(() => {