Skip to content

Commit 9b4e135

Browse files
committed
fix(shared): Add for in cache key for unauthed hooks
1 parent d1a186c commit 9b4e135

File tree

3 files changed

+82
-9
lines changed

3 files changed

+82
-9
lines changed

.changeset/bitter-paths-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/shared': patch
3+
---
4+
5+
Fixes a bug where `usePlans()` would display stale data even if the `for` property has changed.

packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { renderHook, waitFor } from '@testing-library/react';
1+
import { render, renderHook, screen, waitFor } from '@testing-library/react';
2+
import React from 'react';
23
import { beforeEach, describe, expect, it, vi } from 'vitest';
34

45
const mockUser: any = { id: 'user_1' };
@@ -7,7 +8,7 @@ const mockOrganization: any = { id: 'org_1' };
78
const getPlansSpy = vi.fn((args: any) =>
89
Promise.resolve({
910
// pageSize maps to limit; default to 10 if missing
10-
data: Array.from({ length: args.limit ?? args.pageSize ?? 10 }, (_, i) => ({ id: `plan_${i + 1}` })),
11+
data: Array.from({ length: args.limit ?? args.pageSize ?? 10 }, (_, i) => ({ id: `plan_${i + 1}`, for: args.for })),
1112
total_count: 25,
1213
}),
1314
);
@@ -112,4 +113,69 @@ describe('usePlans', () => {
112113
expect(getPlansSpy.mock.calls[0][0]).toStrictEqual({ for: 'organization', pageSize: 3, initialPage: 1 });
113114
expect(result.current.data.length).toBe(3);
114115
});
116+
117+
it('mounts user and organization hooks together and renders their respective data', async () => {
118+
const DualPlans = () => {
119+
const userPlans = usePlans({ initialPage: 1, pageSize: 2 });
120+
const orgPlans = usePlans({ initialPage: 1, pageSize: 2, for: 'organization' } as any);
121+
122+
return (
123+
<>
124+
<div data-testid='user-count'>{userPlans.data.length}</div>
125+
<div data-testid='org-count'>{orgPlans.data.length}</div>
126+
</>
127+
);
128+
};
129+
130+
render(<DualPlans />, { wrapper });
131+
132+
await waitFor(() => expect(screen.getByTestId('user-count').textContent).toBe('2'));
133+
await waitFor(() => expect(screen.getByTestId('org-count').textContent).toBe('2'));
134+
135+
expect(getPlansSpy).toHaveBeenCalledTimes(2);
136+
const calls = getPlansSpy.mock.calls.map(c => c[0]);
137+
expect(calls).toEqual(
138+
expect.arrayContaining([
139+
{ for: 'user', initialPage: 1, pageSize: 2 },
140+
{ for: 'organization', initialPage: 1, pageSize: 2 },
141+
]),
142+
);
143+
144+
// Ensure orgId does not leak into the fetcher params
145+
for (const call of calls) {
146+
expect(call).not.toHaveProperty('orgId');
147+
}
148+
});
149+
150+
it('conditionally renders hooks based on prop passed to render', async () => {
151+
const UserPlansCount = () => {
152+
const userPlans = usePlans({ initialPage: 1, pageSize: 2 });
153+
return <div data-testid='user-type'>{userPlans.data.map(p => p.for)[0]}</div>;
154+
};
155+
156+
const OrgPlansCount = () => {
157+
const orgPlans = usePlans({ initialPage: 1, pageSize: 2, for: 'organization' } as any);
158+
return <div data-testid='org-type'>{orgPlans.data.map(p => p.for)[0]}</div>;
159+
};
160+
161+
const Conditional = ({ showOrg }: { showOrg: boolean }) => (showOrg ? <OrgPlansCount /> : <UserPlansCount />);
162+
163+
const { rerender } = render(<Conditional showOrg={false} />, { wrapper });
164+
165+
await waitFor(() => expect(screen.getByTestId('user-type').textContent).toBe('user'));
166+
expect(getPlansSpy).toHaveBeenCalledTimes(1);
167+
expect(getPlansSpy.mock.calls[0][0]).toStrictEqual({ for: 'user', initialPage: 1, pageSize: 2 });
168+
169+
rerender(<Conditional showOrg />);
170+
171+
await waitFor(() => expect(screen.getByTestId('org-type').textContent).toBe('organization'));
172+
expect(getPlansSpy).toHaveBeenCalledTimes(2);
173+
const calls = getPlansSpy.mock.calls.map(c => c[0]);
174+
expect(calls).toEqual(
175+
expect.arrayContaining([
176+
{ for: 'user', initialPage: 1, pageSize: 2 },
177+
{ for: 'organization', initialPage: 1, pageSize: 2 },
178+
]),
179+
);
180+
});
115181
});

packages/shared/src/react/hooks/createBillingPaginatedHook.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ export function createBillingPaginatedHook<TResource extends ClerkResource, TPar
5353
): PaginatedResources<TResource, T extends { infinite: true } ? true : false> {
5454
const { for: _for, ...paginationParams } = params || ({} as Partial<T>);
5555

56+
const safeFor = _for || 'user';
57+
5658
useAssertWrappedByClerkProvider(hookName);
5759

58-
const fetchFn = useFetcher(_for || 'user');
60+
const fetchFn = useFetcher(safeFor);
5961

6062
const safeValues = useWithSafeValues(paginationParams, {
6163
initialPage: 1,
@@ -74,17 +76,18 @@ export function createBillingPaginatedHook<TResource extends ClerkResource, TPar
7476

7577
clerk.telemetry?.record(eventMethodCalled(hookName));
7678

79+
const isForOrganization = safeFor === 'organization';
80+
7781
const hookParams =
7882
typeof paginationParams === 'undefined'
7983
? undefined
8084
: ({
8185
initialPage: safeValues.initialPage,
8286
pageSize: safeValues.pageSize,
83-
...(options?.unauthenticated ? {} : _for === 'organization' ? { orgId: organization?.id } : {}),
87+
...(options?.unauthenticated ? {} : isForOrganization ? { orgId: organization?.id } : {}),
8488
} as TParams);
8589

86-
const isOrganization = _for === 'organization';
87-
const billingEnabled = isOrganization
90+
const billingEnabled = isForOrganization
8891
? environment?.commerceSettings.billing.organization.enabled
8992
: environment?.commerceSettings.billing.user.enabled;
9093

@@ -102,12 +105,11 @@ export function createBillingPaginatedHook<TResource extends ClerkResource, TPar
102105
},
103106
{
104107
type: resourceType,
105-
// userId: user?.id,
106108
...(options?.unauthenticated
107-
? {}
109+
? { for: _for }
108110
: {
109111
userId: user?.id,
110-
...(_for === 'organization' ? { orgId: organization?.id } : {}),
112+
...(isForOrganization ? { orgId: organization?.id } : {}),
111113
}),
112114
},
113115
);

0 commit comments

Comments
 (0)