feat(shared): Lax revalidation for RQ variant hooks - #7228

Merged
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants
Nov 17, 2025
Merged

feat(shared): Lax revalidation for RQ variant hooks#7228
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants

Conversation

@panteliselef

@panteliselefpanteliselef commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Description

The revalidate() function returned from the new RQ variant hooks will revalidate based on a provided invalidationKey and it will revalidate pagination and infinite mode hooks that match with it.

This changes comes as new "sane" default as this ensures that data will be revalidated across different configuration between the host application and the AIOs.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Breaking Changes

    • Revalidation is now asynchronous and returns a promise.
    • The shape of invalidation keys changed; callers relying on the previous key format must update.
    • Revalidation matching rules relaxed so variant hooks can trigger each other's revalidation.
  • Tests

    • Added extensive tests for revalidation behavior across paginated/infinite hooks, cascading, isolation, and various configurations.

@changeset-bot

changeset-botBot commented Nov 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 415d329

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
NameType
@clerk/sharedPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/chrome-extensionPatch
@clerk/clerk-jsPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/clerk-expoPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/themesPatch
@clerk/typesPatch
@clerk/vuePatch
@clerk/localizationsPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Nov 15, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewCommentNov 17, 2025 0:37am

@coderabbitai

coderabbitaiBot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds extensive revalidation tests across multiple hooks, introduces an InvalidationQueryKey type replacing prior key types, and changes usePagesOrInfinite.rq.tsx's revalidate to async with two sequential cache invalidations using the generalized invalidation key.

Changes

Cohort / File(s)Summary
Revalidation Test Suites
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx, packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts, packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx, packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx
Added tests invoking revalidate, asserting refetches, cache updates, and conditional cascades based on __CLERK_USE_RQ__. Imported act where required.
useAPIKeys Hook Tests
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx
New comprehensive test suite covering initial fetch, revalidation, cascade behavior across paginated/infinite modes, page sizes, filters, and subject isolation.
Pages/Infinite Types
packages/shared/src/react/hooks/usePageOrInfinite.types.ts
Removed Register and AnyQueryKey. Added InvalidationQueryKey = readonly [string, boolean, Record<string, unknown>]. Updated invalidationKey in UsePagesOrInfiniteSignature to use InvalidationQueryKey.
React Query Implementation
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx
Converted revalidate from sync to async; now awaits invalidateQueries(keys.invalidationKey), derives a stable prefix plus -inf variant, then awaits a second invalidateQueries call and returns its result.
Changeset
.changeset/young-impalas-grab.md
Adds changeset describing relaxed revalidation behavior across hook configurations for a patch release of the package.

Sequence Diagram

sequenceDiagram
participant Caller
participant Hook as usePagesOrInfinite.revalidate()
participant QC as QueryClient
rect rgb(245,245,255)
Note over Hook: OLD (sync) - choose one variant to invalidate
Caller->>Hook: revalidate()
alt was infinite
Hook->>QC: invalidateQueries(infiniteQueryKey)
else
Hook->>QC: invalidateQueries(pagesQueryKey)
end
QC-->>Hook: done
Hook-->>Caller: done
end
rect rgb(245,255,245)
Note over Hook: NEW (async) - generalized invalidationKey → sequential invalidations
Caller->>Hook: revalidate()
Hook->>QC: await invalidateQueries(keys.invalidationKey)
QC-->>Hook: resultA
Hook->>Hook: derive stablePrefix + '-inf' + rest
Hook->>QC: await invalidateQueries(derivedInfKey)
QC-->>Hook: resultB
Hook-->>Caller: Promise(resultB)
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect:
    • usePagesOrInfinite.rq.tsx — key-splitting, derived key formation, and return value semantics.
    • usePageOrInfinite.types.ts — consumers adapting to InvalidationQueryKey.
    • New tests — conditional assertions tied to __CLERK_USE_RQ__, proper use of act, and async waits.

Poem

🐰 I nudged two keys to wake the store,

One then the other — I peered for more.
Pages and infinite, both in chase,
Fresh data tumbled into place,
I thumped my foot — the queries race.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: making revalidation behavior more permissive for React Query variant hooks.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elef/improve-revalidations-in-rq-variants

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a973f80 and 415d329.

📒 Files selected for processing (1)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (6)

1-30: LGTM! Clean test setup.

The mock structure and module setup follow testing best practices. The explicit type annotation for getAllSpy enhances readability.


32-38: LGTM! Proper test isolation.

The beforeEach hook correctly resets mocks and query client state, ensuring each test starts with a clean slate.


40-62: LGTM! Solid basic revalidation test.

The test properly handles async operations with act and waitFor, and clearly validates the revalidation flow from initial data to updated data.


101-175: Verify mock parameter names for pageSize and query.

Similar to the previous test, these mocks use parameter names like { pageSize } (line 103) and { query } (line 141) that need to match the actual API signature to ensure the mocks receive the expected values during testing.

The verification script from the previous comment will also check these parameter names.


177-212: LGTM! Subject isolation test validates proper cascade scoping.

This test correctly verifies that revalidation does not cascade across different subjects, which is important for ensuring data isolation. The type assertion on lines 207-211, while complex, safely handles the mock call structure.

However, verify that the { subject } parameter name on line 179 matches the actual API signature, as with the other tests.


64-99: The mock implementation correctly uses valid API parameters.

The test mock at line 66 correctly destructures initialPage from the params object. This parameter is defined in ClerkPaginationParams (which GetAPIKeysParams extends), alongside pageSize. The actual GetAPIKeysParams type expands to include initialPage, pageSize, subject, and query—all of which are properly used across the test cases in this file.

The concern raised in the original review comment does not apply; the mock parameter names match the actual API signature.

Likely an incorrect or invalid review comment.


Comment @coderabbitai help to get the list of available commands and usage tips.

@panteliselefpanteliselef self-assigned this Nov 15, 2025
@panteliselef
panteliselef requested review from a teamNovember 15, 2025 15:11
Base automatically changed from elef/improve-cache-key-construction to mainNovember 17, 2025 11:43
# Conflicts:
#	packages/shared/src/react/hooks/useAPIKeys.ts
#	packages/shared/src/react/hooks/usePageOrInfinite.types.ts
@pkg-pr-new

pkg-pr-newBot commented Nov 17, 2025

Copy link
Copy Markdown

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7228

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7228

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7228

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7228

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7228

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7228

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7228

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7228

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7228

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7228

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7228

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7228

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7228

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7228

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7228

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7228

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7228

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7228

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7228

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7228

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7228

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7228

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7228

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7228

commit: 415d329

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (1)

1-1: Plan revalidation tests nicely pin down cache and isolation behavior

  • "revalidate refetches plans and updates cache" confirms the hook uses the updated revalidate semantics to refresh data and update the cached page.
  • "revalidate for user plans does not refetch organization plans" effectively asserts that invalidation is scoped by the for dimension, so org plans aren’t accidentally refetched when user plans are revalidated. The isRQ gating and calls.every(value => value === 'user') check express this clearly.

Together these tests give good confidence that user vs organization plan queries won’t interfere with each other while still benefiting from the generalized invalidation.

You could optionally also assert that at least one revalidation call has for: 'user' even in the RQ branch (using waitFor on both length and argument shape) to strengthen the assertion, but it’s not strictly necessary.

Also applies to: 207-278

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1)

1-211: Comprehensive revalidation coverage for useApiKeys

This new suite thoroughly exercises useApiKeys revalidation:

  • Verifies that revalidate refreshes data and increments the underlying getAll call count.
  • Confirms cascade behavior between paginated and infinite variants only in RQ mode via __CLERK_USE_RQ__.
  • Checks that cascades respect configuration boundaries (different pageSize, query, and especially different subject values).

The subject-isolation test in particular is a nice guardrail against over-broad invalidation. Overall the structure and use of act/waitFor look solid.

You might optionally tighten the “different pageSize” and “different query filters” tests by asserting that, in the RQ branch, at least one revalidation call corresponds to each configuration (e.g., checking pageSize / query values in getAllSpy.mock.calls) to more directly verify that both related queries are refetched.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4e7a7 and bd87133.

📒 Files selected for processing (7)
  • packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
  • packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2 hunks)
  • packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1 hunks)
  • packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)
packages/clerk-js/src/test/utils.ts (3)
  • renderHook (77-77)
  • waitFor (73-73)
  • act (75-75)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2)
packages/shared/src/types/billing.ts (1)
  • BillingPlanResource (120-187)
packages/shared/src/react/hooks/usePlans.tsx (1)
  • usePlans (8-21)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2)
packages/shared/src/react/hooks/useSubscription.rq.tsx (1)
  • useSubscription (43-99)
packages/shared/src/react/hooks/useSubscription.swr.tsx (1)
  • useSubscription (22-71)
packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)
  • usePagesOrInfinite (21-291)
packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx (1)
  • usePagesOrInfinite (37-237)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (1)

1-1: Revalidation test for subscriptions is well-structured

Importing act and the new "revalidate fetches the latest subscription data" test correctly exercise the hook’s revalidate contract (including async behavior) and mirror patterns used elsewhere in this suite. This should protect against regressions in both RQ and SWR variants.

Also applies to: 217-233

packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)

505-533: Good coverage of basic revalidate refetch semantics

The "refetches current data when revalidate is invoked" test cleanly validates that revalidate performs a fresh fetch and updates data, using act + waitFor in an async-friendly way. This aligns with the new async revalidate implementation and should remain stable across RQ/SWR implementations.


684-724: Cascade revalidation expectations are clearly specified

The "cascades revalidation to related queries only in React Query mode" test nicely captures the intended behavior: in RQ mode, invalidating via the paginated hook also refetches the infinite counterpart, while non-RQ mode keeps revalidation local. The __CLERK_USE_RQ__ gating plus lenient >= 2 assertion make this resilient to minor fetch scheduling differences.

packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)

1-1: Revalidation behavior for billing hooks is well-validated

The added "revalidate behavior" suite does a good job of:

  • Verifying that revalidate actually refreshes paginated data.
  • Asserting that, in RQ mode, revalidation of the paginated hook also triggers the infinite variant while staying local in non-RQ mode.

This ties the higher-level createBillingPaginatedHook behavior back to the updated usePagesOrInfinite revalidation strategy without over-constraining call counts.

Also applies to: 490-550

packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1)

21-22: Invalidation key typing matches the new revalidate implementation

Defining InvalidationQueryKey as [string, boolean, Record<string, unknown>] and wiring keys.invalidationKey to that shape lines up with how usePagesOrInfinite.rq.tsx now destructures and uses the invalidation key (stable prefix, authenticated flag, tracked keys). This tighter typing should help prevent accidentally passing full query keys or mismatched structures into the hook.

It’s worth confirming that all createCacheKeys call sites (and any custom keys objects, if they exist) still satisfy this narrower invalidationKey shape once type-checking and the test suite run.

Also applies to: 28-28

packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)

268-271: Async revalidate with generalized invalidation key looks correct

The new revalidate implementation:

  • First invalidates keys.invalidationKey, which matches the paginated query key prefix.
  • Then derives [stablePrefix + '-inf', ...rest] to invalidate the corresponding infinite queries by prefix.
  • Returns the second invalidation promise, making revalidate awaitable in tests and consumers.

This matches the intended “cascade across pagination/infinite variants sharing the same invalidation key” behavior and is consistent with how tests assert fan-out in RQ mode.

Please ensure this aligns with the exact invalidateQueries prefix-matching semantics in your TanStack Query version (v5 APIs), and that no call sites relied on revalidate being strictly synchronous; the tests added in this PR already cover the new async contract, but a quick type-check / test run will confirm.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

8-14: Consider a more specific type assertion for the mock return value.

The Record<string, unknown> type for data array items is quite loose and could mask type mismatches between the mock and actual API response.

Consider defining a more specific type for the mock API key data, for example:

+type MockAPIKey = { id: string; [key: string]: unknown };+
const getAllSpy = vi.fn(
async () =>
({
data: [],
total_count: 0,
- }) as { data: Array<Record<string, unknown>>; total_count: number },+ }) as { data: Array<MockAPIKey>; total_count: number },
);

64-99: Consider extracting the RQ flag check pattern.

The pattern of checking (globalThis as any).__CLERK_USE_RQ__ and conditionally asserting call counts is repeated across multiple tests (also at lines 128, 166).

Consider extracting a helper function to reduce duplication:

constexpectCallsBasedOnRQMode=async(spy: any,rqCalls: number,nonRqCalls: number)=>{constisRQ=Boolean((globalThisasany).__CLERK_USE_RQ__);if(isRQ){awaitwaitFor(()=>expect(spy.mock.calls.length).toBeGreaterThanOrEqual(rqCalls));}else{awaitwaitFor(()=>expect(spy).toHaveBeenCalledTimes(nonRqCalls));}};

Then use it as:

awaitexpectCallsBasedOnRQMode(getAllSpy,2,1);

177-210: Simplify the subject verification assertion.

Line 209's assertion expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true) is unnecessarily complex.

Apply this diff to make the assertion clearer:

 expect(getAllSpy).toHaveBeenCalledTimes(1);
const subjects = getAllSpy.mock.calls.map(call => (call[0] as { subject?: string })?.subject);
expect(subjects).not.toContain('user_secondary');
- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(subjects[0]).toMatch(/^(user_primary)?$/);

Or even more explicitly:

- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(['user_primary', undefined]).toContain(subjects[0]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd87133 and a973f80.

📒 Files selected for processing (2)
  • .changeset/young-impalas-grab.md (1 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
.changeset/young-impalas-grab.md (1)

1-14: LGTM! Clear documentation of the behavior change.

The changeset clearly describes the relaxed revalidation behavior, and the code example effectively demonstrates how revalidation now works across different hook configurations.

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

32-38: LGTM! Proper test isolation.

The beforeEach hook correctly clears mocks and the query client, ensuring each test starts with a clean state.


40-62: LGTM! Clear revalidation test.

This test effectively verifies that calling revalidate() fetches fresh data from the API.


1-211: Excellent test coverage for revalidation behavior.

The test suite comprehensively covers:

  • Basic revalidation functionality
  • Cascading revalidation across paginated and infinite modes
  • Configuration variations (pageSize, query filters)
  • Subject isolation (negative test)
  • Behavior differences based on the RQ feature flag

All async operations are properly handled with act() and waitFor(), and test isolation is maintained through proper cleanup.

@panteliselef
panteliselef merged commit 00291bc into mainNov 17, 2025
45 checks passed
@panteliselef
panteliselef deleted the elef/improve-revalidations-in-rq-variants branch November 17, 2025 13:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@panteliselef@octoper
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(shared): Lax revalidation for RQ variant hooks - #7228

Merged
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants
Nov 17, 2025
Merged

feat(shared): Lax revalidation for RQ variant hooks#7228
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants

Conversation

@panteliselef

@panteliselefpanteliselef commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Description

The revalidate() function returned from the new RQ variant hooks will revalidate based on a provided invalidationKey and it will revalidate pagination and infinite mode hooks that match with it.

This changes comes as new "sane" default as this ensures that data will be revalidated across different configuration between the host application and the AIOs.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Breaking Changes

    • Revalidation is now asynchronous and returns a promise.
    • The shape of invalidation keys changed; callers relying on the previous key format must update.
    • Revalidation matching rules relaxed so variant hooks can trigger each other's revalidation.
  • Tests

    • Added extensive tests for revalidation behavior across paginated/infinite hooks, cascading, isolation, and various configurations.

@changeset-bot

changeset-botBot commented Nov 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 415d329

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
NameType
@clerk/sharedPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/chrome-extensionPatch
@clerk/clerk-jsPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/clerk-expoPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/themesPatch
@clerk/typesPatch
@clerk/vuePatch
@clerk/localizationsPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Nov 15, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewCommentNov 17, 2025 0:37am

@coderabbitai

coderabbitaiBot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds extensive revalidation tests across multiple hooks, introduces an InvalidationQueryKey type replacing prior key types, and changes usePagesOrInfinite.rq.tsx's revalidate to async with two sequential cache invalidations using the generalized invalidation key.

Changes

Cohort / File(s)Summary
Revalidation Test Suites
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx, packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts, packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx, packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx
Added tests invoking revalidate, asserting refetches, cache updates, and conditional cascades based on __CLERK_USE_RQ__. Imported act where required.
useAPIKeys Hook Tests
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx
New comprehensive test suite covering initial fetch, revalidation, cascade behavior across paginated/infinite modes, page sizes, filters, and subject isolation.
Pages/Infinite Types
packages/shared/src/react/hooks/usePageOrInfinite.types.ts
Removed Register and AnyQueryKey. Added InvalidationQueryKey = readonly [string, boolean, Record<string, unknown>]. Updated invalidationKey in UsePagesOrInfiniteSignature to use InvalidationQueryKey.
React Query Implementation
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx
Converted revalidate from sync to async; now awaits invalidateQueries(keys.invalidationKey), derives a stable prefix plus -inf variant, then awaits a second invalidateQueries call and returns its result.
Changeset
.changeset/young-impalas-grab.md
Adds changeset describing relaxed revalidation behavior across hook configurations for a patch release of the package.

Sequence Diagram

sequenceDiagram
participant Caller
participant Hook as usePagesOrInfinite.revalidate()
participant QC as QueryClient
rect rgb(245,245,255)
Note over Hook: OLD (sync) - choose one variant to invalidate
Caller->>Hook: revalidate()
alt was infinite
Hook->>QC: invalidateQueries(infiniteQueryKey)
else
Hook->>QC: invalidateQueries(pagesQueryKey)
end
QC-->>Hook: done
Hook-->>Caller: done
end
rect rgb(245,255,245)
Note over Hook: NEW (async) - generalized invalidationKey → sequential invalidations
Caller->>Hook: revalidate()
Hook->>QC: await invalidateQueries(keys.invalidationKey)
QC-->>Hook: resultA
Hook->>Hook: derive stablePrefix + '-inf' + rest
Hook->>QC: await invalidateQueries(derivedInfKey)
QC-->>Hook: resultB
Hook-->>Caller: Promise(resultB)
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect:
    • usePagesOrInfinite.rq.tsx — key-splitting, derived key formation, and return value semantics.
    • usePageOrInfinite.types.ts — consumers adapting to InvalidationQueryKey.
    • New tests — conditional assertions tied to __CLERK_USE_RQ__, proper use of act, and async waits.

Poem

🐰 I nudged two keys to wake the store,

One then the other — I peered for more.
Pages and infinite, both in chase,
Fresh data tumbled into place,
I thumped my foot — the queries race.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: making revalidation behavior more permissive for React Query variant hooks.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elef/improve-revalidations-in-rq-variants

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a973f80 and 415d329.

📒 Files selected for processing (1)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (6)

1-30: LGTM! Clean test setup.

The mock structure and module setup follow testing best practices. The explicit type annotation for getAllSpy enhances readability.


32-38: LGTM! Proper test isolation.

The beforeEach hook correctly resets mocks and query client state, ensuring each test starts with a clean slate.


40-62: LGTM! Solid basic revalidation test.

The test properly handles async operations with act and waitFor, and clearly validates the revalidation flow from initial data to updated data.


101-175: Verify mock parameter names for pageSize and query.

Similar to the previous test, these mocks use parameter names like { pageSize } (line 103) and { query } (line 141) that need to match the actual API signature to ensure the mocks receive the expected values during testing.

The verification script from the previous comment will also check these parameter names.


177-212: LGTM! Subject isolation test validates proper cascade scoping.

This test correctly verifies that revalidation does not cascade across different subjects, which is important for ensuring data isolation. The type assertion on lines 207-211, while complex, safely handles the mock call structure.

However, verify that the { subject } parameter name on line 179 matches the actual API signature, as with the other tests.


64-99: The mock implementation correctly uses valid API parameters.

The test mock at line 66 correctly destructures initialPage from the params object. This parameter is defined in ClerkPaginationParams (which GetAPIKeysParams extends), alongside pageSize. The actual GetAPIKeysParams type expands to include initialPage, pageSize, subject, and query—all of which are properly used across the test cases in this file.

The concern raised in the original review comment does not apply; the mock parameter names match the actual API signature.

Likely an incorrect or invalid review comment.


Comment @coderabbitai help to get the list of available commands and usage tips.

@panteliselefpanteliselef self-assigned this Nov 15, 2025
@panteliselef
panteliselef requested review from a teamNovember 15, 2025 15:11
Base automatically changed from elef/improve-cache-key-construction to mainNovember 17, 2025 11:43
# Conflicts:
#	packages/shared/src/react/hooks/useAPIKeys.ts
#	packages/shared/src/react/hooks/usePageOrInfinite.types.ts
@pkg-pr-new

pkg-pr-newBot commented Nov 17, 2025

Copy link
Copy Markdown

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7228

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7228

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7228

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7228

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7228

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7228

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7228

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7228

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7228

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7228

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7228

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7228

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7228

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7228

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7228

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7228

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7228

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7228

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7228

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7228

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7228

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7228

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7228

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7228

commit: 415d329

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (1)

1-1: Plan revalidation tests nicely pin down cache and isolation behavior

  • "revalidate refetches plans and updates cache" confirms the hook uses the updated revalidate semantics to refresh data and update the cached page.
  • "revalidate for user plans does not refetch organization plans" effectively asserts that invalidation is scoped by the for dimension, so org plans aren’t accidentally refetched when user plans are revalidated. The isRQ gating and calls.every(value => value === 'user') check express this clearly.

Together these tests give good confidence that user vs organization plan queries won’t interfere with each other while still benefiting from the generalized invalidation.

You could optionally also assert that at least one revalidation call has for: 'user' even in the RQ branch (using waitFor on both length and argument shape) to strengthen the assertion, but it’s not strictly necessary.

Also applies to: 207-278

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1)

1-211: Comprehensive revalidation coverage for useApiKeys

This new suite thoroughly exercises useApiKeys revalidation:

  • Verifies that revalidate refreshes data and increments the underlying getAll call count.
  • Confirms cascade behavior between paginated and infinite variants only in RQ mode via __CLERK_USE_RQ__.
  • Checks that cascades respect configuration boundaries (different pageSize, query, and especially different subject values).

The subject-isolation test in particular is a nice guardrail against over-broad invalidation. Overall the structure and use of act/waitFor look solid.

You might optionally tighten the “different pageSize” and “different query filters” tests by asserting that, in the RQ branch, at least one revalidation call corresponds to each configuration (e.g., checking pageSize / query values in getAllSpy.mock.calls) to more directly verify that both related queries are refetched.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4e7a7 and bd87133.

📒 Files selected for processing (7)
  • packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
  • packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2 hunks)
  • packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1 hunks)
  • packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)
packages/clerk-js/src/test/utils.ts (3)
  • renderHook (77-77)
  • waitFor (73-73)
  • act (75-75)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2)
packages/shared/src/types/billing.ts (1)
  • BillingPlanResource (120-187)
packages/shared/src/react/hooks/usePlans.tsx (1)
  • usePlans (8-21)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2)
packages/shared/src/react/hooks/useSubscription.rq.tsx (1)
  • useSubscription (43-99)
packages/shared/src/react/hooks/useSubscription.swr.tsx (1)
  • useSubscription (22-71)
packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)
  • usePagesOrInfinite (21-291)
packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx (1)
  • usePagesOrInfinite (37-237)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (1)

1-1: Revalidation test for subscriptions is well-structured

Importing act and the new "revalidate fetches the latest subscription data" test correctly exercise the hook’s revalidate contract (including async behavior) and mirror patterns used elsewhere in this suite. This should protect against regressions in both RQ and SWR variants.

Also applies to: 217-233

packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)

505-533: Good coverage of basic revalidate refetch semantics

The "refetches current data when revalidate is invoked" test cleanly validates that revalidate performs a fresh fetch and updates data, using act + waitFor in an async-friendly way. This aligns with the new async revalidate implementation and should remain stable across RQ/SWR implementations.


684-724: Cascade revalidation expectations are clearly specified

The "cascades revalidation to related queries only in React Query mode" test nicely captures the intended behavior: in RQ mode, invalidating via the paginated hook also refetches the infinite counterpart, while non-RQ mode keeps revalidation local. The __CLERK_USE_RQ__ gating plus lenient >= 2 assertion make this resilient to minor fetch scheduling differences.

packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)

1-1: Revalidation behavior for billing hooks is well-validated

The added "revalidate behavior" suite does a good job of:

  • Verifying that revalidate actually refreshes paginated data.
  • Asserting that, in RQ mode, revalidation of the paginated hook also triggers the infinite variant while staying local in non-RQ mode.

This ties the higher-level createBillingPaginatedHook behavior back to the updated usePagesOrInfinite revalidation strategy without over-constraining call counts.

Also applies to: 490-550

packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1)

21-22: Invalidation key typing matches the new revalidate implementation

Defining InvalidationQueryKey as [string, boolean, Record<string, unknown>] and wiring keys.invalidationKey to that shape lines up with how usePagesOrInfinite.rq.tsx now destructures and uses the invalidation key (stable prefix, authenticated flag, tracked keys). This tighter typing should help prevent accidentally passing full query keys or mismatched structures into the hook.

It’s worth confirming that all createCacheKeys call sites (and any custom keys objects, if they exist) still satisfy this narrower invalidationKey shape once type-checking and the test suite run.

Also applies to: 28-28

packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)

268-271: Async revalidate with generalized invalidation key looks correct

The new revalidate implementation:

  • First invalidates keys.invalidationKey, which matches the paginated query key prefix.
  • Then derives [stablePrefix + '-inf', ...rest] to invalidate the corresponding infinite queries by prefix.
  • Returns the second invalidation promise, making revalidate awaitable in tests and consumers.

This matches the intended “cascade across pagination/infinite variants sharing the same invalidation key” behavior and is consistent with how tests assert fan-out in RQ mode.

Please ensure this aligns with the exact invalidateQueries prefix-matching semantics in your TanStack Query version (v5 APIs), and that no call sites relied on revalidate being strictly synchronous; the tests added in this PR already cover the new async contract, but a quick type-check / test run will confirm.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

8-14: Consider a more specific type assertion for the mock return value.

The Record<string, unknown> type for data array items is quite loose and could mask type mismatches between the mock and actual API response.

Consider defining a more specific type for the mock API key data, for example:

+type MockAPIKey = { id: string; [key: string]: unknown };+
const getAllSpy = vi.fn(
async () =>
({
data: [],
total_count: 0,
- }) as { data: Array<Record<string, unknown>>; total_count: number },+ }) as { data: Array<MockAPIKey>; total_count: number },
);

64-99: Consider extracting the RQ flag check pattern.

The pattern of checking (globalThis as any).__CLERK_USE_RQ__ and conditionally asserting call counts is repeated across multiple tests (also at lines 128, 166).

Consider extracting a helper function to reduce duplication:

constexpectCallsBasedOnRQMode=async(spy: any,rqCalls: number,nonRqCalls: number)=>{constisRQ=Boolean((globalThisasany).__CLERK_USE_RQ__);if(isRQ){awaitwaitFor(()=>expect(spy.mock.calls.length).toBeGreaterThanOrEqual(rqCalls));}else{awaitwaitFor(()=>expect(spy).toHaveBeenCalledTimes(nonRqCalls));}};

Then use it as:

awaitexpectCallsBasedOnRQMode(getAllSpy,2,1);

177-210: Simplify the subject verification assertion.

Line 209's assertion expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true) is unnecessarily complex.

Apply this diff to make the assertion clearer:

 expect(getAllSpy).toHaveBeenCalledTimes(1);
const subjects = getAllSpy.mock.calls.map(call => (call[0] as { subject?: string })?.subject);
expect(subjects).not.toContain('user_secondary');
- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(subjects[0]).toMatch(/^(user_primary)?$/);

Or even more explicitly:

- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(['user_primary', undefined]).toContain(subjects[0]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd87133 and a973f80.

📒 Files selected for processing (2)
  • .changeset/young-impalas-grab.md (1 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
.changeset/young-impalas-grab.md (1)

1-14: LGTM! Clear documentation of the behavior change.

The changeset clearly describes the relaxed revalidation behavior, and the code example effectively demonstrates how revalidation now works across different hook configurations.

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

32-38: LGTM! Proper test isolation.

The beforeEach hook correctly clears mocks and the query client, ensuring each test starts with a clean state.


40-62: LGTM! Clear revalidation test.

This test effectively verifies that calling revalidate() fetches fresh data from the API.


1-211: Excellent test coverage for revalidation behavior.

The test suite comprehensively covers:

  • Basic revalidation functionality
  • Cascading revalidation across paginated and infinite modes
  • Configuration variations (pageSize, query filters)
  • Subject isolation (negative test)
  • Behavior differences based on the RQ feature flag

All async operations are properly handled with act() and waitFor(), and test isolation is maintained through proper cleanup.

@panteliselef
panteliselef merged commit 00291bc into mainNov 17, 2025
45 checks passed
@panteliselef
panteliselef deleted the elef/improve-revalidations-in-rq-variants branch November 17, 2025 13:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@panteliselef@octoper
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(shared): Lax revalidation for RQ variant hooks - #7228

Merged
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants
Nov 17, 2025
Merged

feat(shared): Lax revalidation for RQ variant hooks#7228
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants

Conversation

@panteliselef

@panteliselefpanteliselef commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Description

The revalidate() function returned from the new RQ variant hooks will revalidate based on a provided invalidationKey and it will revalidate pagination and infinite mode hooks that match with it.

This changes comes as new "sane" default as this ensures that data will be revalidated across different configuration between the host application and the AIOs.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Breaking Changes

    • Revalidation is now asynchronous and returns a promise.
    • The shape of invalidation keys changed; callers relying on the previous key format must update.
    • Revalidation matching rules relaxed so variant hooks can trigger each other's revalidation.
  • Tests

    • Added extensive tests for revalidation behavior across paginated/infinite hooks, cascading, isolation, and various configurations.

@changeset-bot

changeset-botBot commented Nov 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 415d329

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
NameType
@clerk/sharedPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/chrome-extensionPatch
@clerk/clerk-jsPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/clerk-expoPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/themesPatch
@clerk/typesPatch
@clerk/vuePatch
@clerk/localizationsPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Nov 15, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewCommentNov 17, 2025 0:37am

@coderabbitai

coderabbitaiBot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds extensive revalidation tests across multiple hooks, introduces an InvalidationQueryKey type replacing prior key types, and changes usePagesOrInfinite.rq.tsx's revalidate to async with two sequential cache invalidations using the generalized invalidation key.

Changes

Cohort / File(s)Summary
Revalidation Test Suites
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx, packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts, packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx, packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx
Added tests invoking revalidate, asserting refetches, cache updates, and conditional cascades based on __CLERK_USE_RQ__. Imported act where required.
useAPIKeys Hook Tests
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx
New comprehensive test suite covering initial fetch, revalidation, cascade behavior across paginated/infinite modes, page sizes, filters, and subject isolation.
Pages/Infinite Types
packages/shared/src/react/hooks/usePageOrInfinite.types.ts
Removed Register and AnyQueryKey. Added InvalidationQueryKey = readonly [string, boolean, Record<string, unknown>]. Updated invalidationKey in UsePagesOrInfiniteSignature to use InvalidationQueryKey.
React Query Implementation
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx
Converted revalidate from sync to async; now awaits invalidateQueries(keys.invalidationKey), derives a stable prefix plus -inf variant, then awaits a second invalidateQueries call and returns its result.
Changeset
.changeset/young-impalas-grab.md
Adds changeset describing relaxed revalidation behavior across hook configurations for a patch release of the package.

Sequence Diagram

sequenceDiagram
participant Caller
participant Hook as usePagesOrInfinite.revalidate()
participant QC as QueryClient
rect rgb(245,245,255)
Note over Hook: OLD (sync) - choose one variant to invalidate
Caller->>Hook: revalidate()
alt was infinite
Hook->>QC: invalidateQueries(infiniteQueryKey)
else
Hook->>QC: invalidateQueries(pagesQueryKey)
end
QC-->>Hook: done
Hook-->>Caller: done
end
rect rgb(245,255,245)
Note over Hook: NEW (async) - generalized invalidationKey → sequential invalidations
Caller->>Hook: revalidate()
Hook->>QC: await invalidateQueries(keys.invalidationKey)
QC-->>Hook: resultA
Hook->>Hook: derive stablePrefix + '-inf' + rest
Hook->>QC: await invalidateQueries(derivedInfKey)
QC-->>Hook: resultB
Hook-->>Caller: Promise(resultB)
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect:
    • usePagesOrInfinite.rq.tsx — key-splitting, derived key formation, and return value semantics.
    • usePageOrInfinite.types.ts — consumers adapting to InvalidationQueryKey.
    • New tests — conditional assertions tied to __CLERK_USE_RQ__, proper use of act, and async waits.

Poem

🐰 I nudged two keys to wake the store,

One then the other — I peered for more.
Pages and infinite, both in chase,
Fresh data tumbled into place,
I thumped my foot — the queries race.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: making revalidation behavior more permissive for React Query variant hooks.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elef/improve-revalidations-in-rq-variants

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a973f80 and 415d329.

📒 Files selected for processing (1)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (6)

1-30: LGTM! Clean test setup.

The mock structure and module setup follow testing best practices. The explicit type annotation for getAllSpy enhances readability.


32-38: LGTM! Proper test isolation.

The beforeEach hook correctly resets mocks and query client state, ensuring each test starts with a clean slate.


40-62: LGTM! Solid basic revalidation test.

The test properly handles async operations with act and waitFor, and clearly validates the revalidation flow from initial data to updated data.


101-175: Verify mock parameter names for pageSize and query.

Similar to the previous test, these mocks use parameter names like { pageSize } (line 103) and { query } (line 141) that need to match the actual API signature to ensure the mocks receive the expected values during testing.

The verification script from the previous comment will also check these parameter names.


177-212: LGTM! Subject isolation test validates proper cascade scoping.

This test correctly verifies that revalidation does not cascade across different subjects, which is important for ensuring data isolation. The type assertion on lines 207-211, while complex, safely handles the mock call structure.

However, verify that the { subject } parameter name on line 179 matches the actual API signature, as with the other tests.


64-99: The mock implementation correctly uses valid API parameters.

The test mock at line 66 correctly destructures initialPage from the params object. This parameter is defined in ClerkPaginationParams (which GetAPIKeysParams extends), alongside pageSize. The actual GetAPIKeysParams type expands to include initialPage, pageSize, subject, and query—all of which are properly used across the test cases in this file.

The concern raised in the original review comment does not apply; the mock parameter names match the actual API signature.

Likely an incorrect or invalid review comment.


Comment @coderabbitai help to get the list of available commands and usage tips.

@panteliselefpanteliselef self-assigned this Nov 15, 2025
@panteliselef
panteliselef requested review from a teamNovember 15, 2025 15:11
Base automatically changed from elef/improve-cache-key-construction to mainNovember 17, 2025 11:43
# Conflicts:
#	packages/shared/src/react/hooks/useAPIKeys.ts
#	packages/shared/src/react/hooks/usePageOrInfinite.types.ts
@pkg-pr-new

pkg-pr-newBot commented Nov 17, 2025

Copy link
Copy Markdown

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7228

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7228

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7228

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7228

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7228

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7228

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7228

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7228

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7228

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7228

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7228

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7228

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7228

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7228

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7228

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7228

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7228

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7228

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7228

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7228

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7228

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7228

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7228

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7228

commit: 415d329

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (1)

1-1: Plan revalidation tests nicely pin down cache and isolation behavior

  • "revalidate refetches plans and updates cache" confirms the hook uses the updated revalidate semantics to refresh data and update the cached page.
  • "revalidate for user plans does not refetch organization plans" effectively asserts that invalidation is scoped by the for dimension, so org plans aren’t accidentally refetched when user plans are revalidated. The isRQ gating and calls.every(value => value === 'user') check express this clearly.

Together these tests give good confidence that user vs organization plan queries won’t interfere with each other while still benefiting from the generalized invalidation.

You could optionally also assert that at least one revalidation call has for: 'user' even in the RQ branch (using waitFor on both length and argument shape) to strengthen the assertion, but it’s not strictly necessary.

Also applies to: 207-278

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1)

1-211: Comprehensive revalidation coverage for useApiKeys

This new suite thoroughly exercises useApiKeys revalidation:

  • Verifies that revalidate refreshes data and increments the underlying getAll call count.
  • Confirms cascade behavior between paginated and infinite variants only in RQ mode via __CLERK_USE_RQ__.
  • Checks that cascades respect configuration boundaries (different pageSize, query, and especially different subject values).

The subject-isolation test in particular is a nice guardrail against over-broad invalidation. Overall the structure and use of act/waitFor look solid.

You might optionally tighten the “different pageSize” and “different query filters” tests by asserting that, in the RQ branch, at least one revalidation call corresponds to each configuration (e.g., checking pageSize / query values in getAllSpy.mock.calls) to more directly verify that both related queries are refetched.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4e7a7 and bd87133.

📒 Files selected for processing (7)
  • packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
  • packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2 hunks)
  • packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1 hunks)
  • packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)
packages/clerk-js/src/test/utils.ts (3)
  • renderHook (77-77)
  • waitFor (73-73)
  • act (75-75)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2)
packages/shared/src/types/billing.ts (1)
  • BillingPlanResource (120-187)
packages/shared/src/react/hooks/usePlans.tsx (1)
  • usePlans (8-21)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2)
packages/shared/src/react/hooks/useSubscription.rq.tsx (1)
  • useSubscription (43-99)
packages/shared/src/react/hooks/useSubscription.swr.tsx (1)
  • useSubscription (22-71)
packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)
  • usePagesOrInfinite (21-291)
packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx (1)
  • usePagesOrInfinite (37-237)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (1)

1-1: Revalidation test for subscriptions is well-structured

Importing act and the new "revalidate fetches the latest subscription data" test correctly exercise the hook’s revalidate contract (including async behavior) and mirror patterns used elsewhere in this suite. This should protect against regressions in both RQ and SWR variants.

Also applies to: 217-233

packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)

505-533: Good coverage of basic revalidate refetch semantics

The "refetches current data when revalidate is invoked" test cleanly validates that revalidate performs a fresh fetch and updates data, using act + waitFor in an async-friendly way. This aligns with the new async revalidate implementation and should remain stable across RQ/SWR implementations.


684-724: Cascade revalidation expectations are clearly specified

The "cascades revalidation to related queries only in React Query mode" test nicely captures the intended behavior: in RQ mode, invalidating via the paginated hook also refetches the infinite counterpart, while non-RQ mode keeps revalidation local. The __CLERK_USE_RQ__ gating plus lenient >= 2 assertion make this resilient to minor fetch scheduling differences.

packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)

1-1: Revalidation behavior for billing hooks is well-validated

The added "revalidate behavior" suite does a good job of:

  • Verifying that revalidate actually refreshes paginated data.
  • Asserting that, in RQ mode, revalidation of the paginated hook also triggers the infinite variant while staying local in non-RQ mode.

This ties the higher-level createBillingPaginatedHook behavior back to the updated usePagesOrInfinite revalidation strategy without over-constraining call counts.

Also applies to: 490-550

packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1)

21-22: Invalidation key typing matches the new revalidate implementation

Defining InvalidationQueryKey as [string, boolean, Record<string, unknown>] and wiring keys.invalidationKey to that shape lines up with how usePagesOrInfinite.rq.tsx now destructures and uses the invalidation key (stable prefix, authenticated flag, tracked keys). This tighter typing should help prevent accidentally passing full query keys or mismatched structures into the hook.

It’s worth confirming that all createCacheKeys call sites (and any custom keys objects, if they exist) still satisfy this narrower invalidationKey shape once type-checking and the test suite run.

Also applies to: 28-28

packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)

268-271: Async revalidate with generalized invalidation key looks correct

The new revalidate implementation:

  • First invalidates keys.invalidationKey, which matches the paginated query key prefix.
  • Then derives [stablePrefix + '-inf', ...rest] to invalidate the corresponding infinite queries by prefix.
  • Returns the second invalidation promise, making revalidate awaitable in tests and consumers.

This matches the intended “cascade across pagination/infinite variants sharing the same invalidation key” behavior and is consistent with how tests assert fan-out in RQ mode.

Please ensure this aligns with the exact invalidateQueries prefix-matching semantics in your TanStack Query version (v5 APIs), and that no call sites relied on revalidate being strictly synchronous; the tests added in this PR already cover the new async contract, but a quick type-check / test run will confirm.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

8-14: Consider a more specific type assertion for the mock return value.

The Record<string, unknown> type for data array items is quite loose and could mask type mismatches between the mock and actual API response.

Consider defining a more specific type for the mock API key data, for example:

+type MockAPIKey = { id: string; [key: string]: unknown };+
const getAllSpy = vi.fn(
async () =>
({
data: [],
total_count: 0,
- }) as { data: Array<Record<string, unknown>>; total_count: number },+ }) as { data: Array<MockAPIKey>; total_count: number },
);

64-99: Consider extracting the RQ flag check pattern.

The pattern of checking (globalThis as any).__CLERK_USE_RQ__ and conditionally asserting call counts is repeated across multiple tests (also at lines 128, 166).

Consider extracting a helper function to reduce duplication:

constexpectCallsBasedOnRQMode=async(spy: any,rqCalls: number,nonRqCalls: number)=>{constisRQ=Boolean((globalThisasany).__CLERK_USE_RQ__);if(isRQ){awaitwaitFor(()=>expect(spy.mock.calls.length).toBeGreaterThanOrEqual(rqCalls));}else{awaitwaitFor(()=>expect(spy).toHaveBeenCalledTimes(nonRqCalls));}};

Then use it as:

awaitexpectCallsBasedOnRQMode(getAllSpy,2,1);

177-210: Simplify the subject verification assertion.

Line 209's assertion expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true) is unnecessarily complex.

Apply this diff to make the assertion clearer:

 expect(getAllSpy).toHaveBeenCalledTimes(1);
const subjects = getAllSpy.mock.calls.map(call => (call[0] as { subject?: string })?.subject);
expect(subjects).not.toContain('user_secondary');
- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(subjects[0]).toMatch(/^(user_primary)?$/);

Or even more explicitly:

- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(['user_primary', undefined]).toContain(subjects[0]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd87133 and a973f80.

📒 Files selected for processing (2)
  • .changeset/young-impalas-grab.md (1 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
.changeset/young-impalas-grab.md (1)

1-14: LGTM! Clear documentation of the behavior change.

The changeset clearly describes the relaxed revalidation behavior, and the code example effectively demonstrates how revalidation now works across different hook configurations.

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

32-38: LGTM! Proper test isolation.

The beforeEach hook correctly clears mocks and the query client, ensuring each test starts with a clean state.


40-62: LGTM! Clear revalidation test.

This test effectively verifies that calling revalidate() fetches fresh data from the API.


1-211: Excellent test coverage for revalidation behavior.

The test suite comprehensively covers:

  • Basic revalidation functionality
  • Cascading revalidation across paginated and infinite modes
  • Configuration variations (pageSize, query filters)
  • Subject isolation (negative test)
  • Behavior differences based on the RQ feature flag

All async operations are properly handled with act() and waitFor(), and test isolation is maintained through proper cleanup.

@panteliselef
panteliselef merged commit 00291bc into mainNov 17, 2025
45 checks passed
@panteliselef
panteliselef deleted the elef/improve-revalidations-in-rq-variants branch November 17, 2025 13:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@panteliselef@octoper
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(shared): Lax revalidation for RQ variant hooks - #7228

Merged
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants
Nov 17, 2025
Merged

feat(shared): Lax revalidation for RQ variant hooks#7228
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants

Conversation

@panteliselef

@panteliselefpanteliselef commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Description

The revalidate() function returned from the new RQ variant hooks will revalidate based on a provided invalidationKey and it will revalidate pagination and infinite mode hooks that match with it.

This changes comes as new "sane" default as this ensures that data will be revalidated across different configuration between the host application and the AIOs.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Breaking Changes

    • Revalidation is now asynchronous and returns a promise.
    • The shape of invalidation keys changed; callers relying on the previous key format must update.
    • Revalidation matching rules relaxed so variant hooks can trigger each other's revalidation.
  • Tests

    • Added extensive tests for revalidation behavior across paginated/infinite hooks, cascading, isolation, and various configurations.

@changeset-bot

changeset-botBot commented Nov 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 415d329

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
NameType
@clerk/sharedPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/chrome-extensionPatch
@clerk/clerk-jsPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/clerk-expoPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/themesPatch
@clerk/typesPatch
@clerk/vuePatch
@clerk/localizationsPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Nov 15, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewCommentNov 17, 2025 0:37am

@coderabbitai

coderabbitaiBot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds extensive revalidation tests across multiple hooks, introduces an InvalidationQueryKey type replacing prior key types, and changes usePagesOrInfinite.rq.tsx's revalidate to async with two sequential cache invalidations using the generalized invalidation key.

Changes

Cohort / File(s)Summary
Revalidation Test Suites
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx, packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts, packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx, packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx
Added tests invoking revalidate, asserting refetches, cache updates, and conditional cascades based on __CLERK_USE_RQ__. Imported act where required.
useAPIKeys Hook Tests
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx
New comprehensive test suite covering initial fetch, revalidation, cascade behavior across paginated/infinite modes, page sizes, filters, and subject isolation.
Pages/Infinite Types
packages/shared/src/react/hooks/usePageOrInfinite.types.ts
Removed Register and AnyQueryKey. Added InvalidationQueryKey = readonly [string, boolean, Record<string, unknown>]. Updated invalidationKey in UsePagesOrInfiniteSignature to use InvalidationQueryKey.
React Query Implementation
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx
Converted revalidate from sync to async; now awaits invalidateQueries(keys.invalidationKey), derives a stable prefix plus -inf variant, then awaits a second invalidateQueries call and returns its result.
Changeset
.changeset/young-impalas-grab.md
Adds changeset describing relaxed revalidation behavior across hook configurations for a patch release of the package.

Sequence Diagram

sequenceDiagram
participant Caller
participant Hook as usePagesOrInfinite.revalidate()
participant QC as QueryClient
rect rgb(245,245,255)
Note over Hook: OLD (sync) - choose one variant to invalidate
Caller->>Hook: revalidate()
alt was infinite
Hook->>QC: invalidateQueries(infiniteQueryKey)
else
Hook->>QC: invalidateQueries(pagesQueryKey)
end
QC-->>Hook: done
Hook-->>Caller: done
end
rect rgb(245,255,245)
Note over Hook: NEW (async) - generalized invalidationKey → sequential invalidations
Caller->>Hook: revalidate()
Hook->>QC: await invalidateQueries(keys.invalidationKey)
QC-->>Hook: resultA
Hook->>Hook: derive stablePrefix + '-inf' + rest
Hook->>QC: await invalidateQueries(derivedInfKey)
QC-->>Hook: resultB
Hook-->>Caller: Promise(resultB)
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect:
    • usePagesOrInfinite.rq.tsx — key-splitting, derived key formation, and return value semantics.
    • usePageOrInfinite.types.ts — consumers adapting to InvalidationQueryKey.
    • New tests — conditional assertions tied to __CLERK_USE_RQ__, proper use of act, and async waits.

Poem

🐰 I nudged two keys to wake the store,

One then the other — I peered for more.
Pages and infinite, both in chase,
Fresh data tumbled into place,
I thumped my foot — the queries race.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: making revalidation behavior more permissive for React Query variant hooks.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elef/improve-revalidations-in-rq-variants

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a973f80 and 415d329.

📒 Files selected for processing (1)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (6)

1-30: LGTM! Clean test setup.

The mock structure and module setup follow testing best practices. The explicit type annotation for getAllSpy enhances readability.


32-38: LGTM! Proper test isolation.

The beforeEach hook correctly resets mocks and query client state, ensuring each test starts with a clean slate.


40-62: LGTM! Solid basic revalidation test.

The test properly handles async operations with act and waitFor, and clearly validates the revalidation flow from initial data to updated data.


101-175: Verify mock parameter names for pageSize and query.

Similar to the previous test, these mocks use parameter names like { pageSize } (line 103) and { query } (line 141) that need to match the actual API signature to ensure the mocks receive the expected values during testing.

The verification script from the previous comment will also check these parameter names.


177-212: LGTM! Subject isolation test validates proper cascade scoping.

This test correctly verifies that revalidation does not cascade across different subjects, which is important for ensuring data isolation. The type assertion on lines 207-211, while complex, safely handles the mock call structure.

However, verify that the { subject } parameter name on line 179 matches the actual API signature, as with the other tests.


64-99: The mock implementation correctly uses valid API parameters.

The test mock at line 66 correctly destructures initialPage from the params object. This parameter is defined in ClerkPaginationParams (which GetAPIKeysParams extends), alongside pageSize. The actual GetAPIKeysParams type expands to include initialPage, pageSize, subject, and query—all of which are properly used across the test cases in this file.

The concern raised in the original review comment does not apply; the mock parameter names match the actual API signature.

Likely an incorrect or invalid review comment.


Comment @coderabbitai help to get the list of available commands and usage tips.

@panteliselefpanteliselef self-assigned this Nov 15, 2025
@panteliselef
panteliselef requested review from a teamNovember 15, 2025 15:11
Base automatically changed from elef/improve-cache-key-construction to mainNovember 17, 2025 11:43
# Conflicts:
#	packages/shared/src/react/hooks/useAPIKeys.ts
#	packages/shared/src/react/hooks/usePageOrInfinite.types.ts
@pkg-pr-new

pkg-pr-newBot commented Nov 17, 2025

Copy link
Copy Markdown

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7228

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7228

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7228

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7228

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7228

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7228

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7228

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7228

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7228

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7228

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7228

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7228

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7228

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7228

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7228

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7228

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7228

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7228

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7228

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7228

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7228

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7228

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7228

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7228

commit: 415d329

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (1)

1-1: Plan revalidation tests nicely pin down cache and isolation behavior

  • "revalidate refetches plans and updates cache" confirms the hook uses the updated revalidate semantics to refresh data and update the cached page.
  • "revalidate for user plans does not refetch organization plans" effectively asserts that invalidation is scoped by the for dimension, so org plans aren’t accidentally refetched when user plans are revalidated. The isRQ gating and calls.every(value => value === 'user') check express this clearly.

Together these tests give good confidence that user vs organization plan queries won’t interfere with each other while still benefiting from the generalized invalidation.

You could optionally also assert that at least one revalidation call has for: 'user' even in the RQ branch (using waitFor on both length and argument shape) to strengthen the assertion, but it’s not strictly necessary.

Also applies to: 207-278

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1)

1-211: Comprehensive revalidation coverage for useApiKeys

This new suite thoroughly exercises useApiKeys revalidation:

  • Verifies that revalidate refreshes data and increments the underlying getAll call count.
  • Confirms cascade behavior between paginated and infinite variants only in RQ mode via __CLERK_USE_RQ__.
  • Checks that cascades respect configuration boundaries (different pageSize, query, and especially different subject values).

The subject-isolation test in particular is a nice guardrail against over-broad invalidation. Overall the structure and use of act/waitFor look solid.

You might optionally tighten the “different pageSize” and “different query filters” tests by asserting that, in the RQ branch, at least one revalidation call corresponds to each configuration (e.g., checking pageSize / query values in getAllSpy.mock.calls) to more directly verify that both related queries are refetched.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4e7a7 and bd87133.

📒 Files selected for processing (7)
  • packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
  • packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2 hunks)
  • packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1 hunks)
  • packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)
packages/clerk-js/src/test/utils.ts (3)
  • renderHook (77-77)
  • waitFor (73-73)
  • act (75-75)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2)
packages/shared/src/types/billing.ts (1)
  • BillingPlanResource (120-187)
packages/shared/src/react/hooks/usePlans.tsx (1)
  • usePlans (8-21)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2)
packages/shared/src/react/hooks/useSubscription.rq.tsx (1)
  • useSubscription (43-99)
packages/shared/src/react/hooks/useSubscription.swr.tsx (1)
  • useSubscription (22-71)
packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)
  • usePagesOrInfinite (21-291)
packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx (1)
  • usePagesOrInfinite (37-237)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (1)

1-1: Revalidation test for subscriptions is well-structured

Importing act and the new "revalidate fetches the latest subscription data" test correctly exercise the hook’s revalidate contract (including async behavior) and mirror patterns used elsewhere in this suite. This should protect against regressions in both RQ and SWR variants.

Also applies to: 217-233

packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)

505-533: Good coverage of basic revalidate refetch semantics

The "refetches current data when revalidate is invoked" test cleanly validates that revalidate performs a fresh fetch and updates data, using act + waitFor in an async-friendly way. This aligns with the new async revalidate implementation and should remain stable across RQ/SWR implementations.


684-724: Cascade revalidation expectations are clearly specified

The "cascades revalidation to related queries only in React Query mode" test nicely captures the intended behavior: in RQ mode, invalidating via the paginated hook also refetches the infinite counterpart, while non-RQ mode keeps revalidation local. The __CLERK_USE_RQ__ gating plus lenient >= 2 assertion make this resilient to minor fetch scheduling differences.

packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)

1-1: Revalidation behavior for billing hooks is well-validated

The added "revalidate behavior" suite does a good job of:

  • Verifying that revalidate actually refreshes paginated data.
  • Asserting that, in RQ mode, revalidation of the paginated hook also triggers the infinite variant while staying local in non-RQ mode.

This ties the higher-level createBillingPaginatedHook behavior back to the updated usePagesOrInfinite revalidation strategy without over-constraining call counts.

Also applies to: 490-550

packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1)

21-22: Invalidation key typing matches the new revalidate implementation

Defining InvalidationQueryKey as [string, boolean, Record<string, unknown>] and wiring keys.invalidationKey to that shape lines up with how usePagesOrInfinite.rq.tsx now destructures and uses the invalidation key (stable prefix, authenticated flag, tracked keys). This tighter typing should help prevent accidentally passing full query keys or mismatched structures into the hook.

It’s worth confirming that all createCacheKeys call sites (and any custom keys objects, if they exist) still satisfy this narrower invalidationKey shape once type-checking and the test suite run.

Also applies to: 28-28

packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)

268-271: Async revalidate with generalized invalidation key looks correct

The new revalidate implementation:

  • First invalidates keys.invalidationKey, which matches the paginated query key prefix.
  • Then derives [stablePrefix + '-inf', ...rest] to invalidate the corresponding infinite queries by prefix.
  • Returns the second invalidation promise, making revalidate awaitable in tests and consumers.

This matches the intended “cascade across pagination/infinite variants sharing the same invalidation key” behavior and is consistent with how tests assert fan-out in RQ mode.

Please ensure this aligns with the exact invalidateQueries prefix-matching semantics in your TanStack Query version (v5 APIs), and that no call sites relied on revalidate being strictly synchronous; the tests added in this PR already cover the new async contract, but a quick type-check / test run will confirm.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

8-14: Consider a more specific type assertion for the mock return value.

The Record<string, unknown> type for data array items is quite loose and could mask type mismatches between the mock and actual API response.

Consider defining a more specific type for the mock API key data, for example:

+type MockAPIKey = { id: string; [key: string]: unknown };+
const getAllSpy = vi.fn(
async () =>
({
data: [],
total_count: 0,
- }) as { data: Array<Record<string, unknown>>; total_count: number },+ }) as { data: Array<MockAPIKey>; total_count: number },
);

64-99: Consider extracting the RQ flag check pattern.

The pattern of checking (globalThis as any).__CLERK_USE_RQ__ and conditionally asserting call counts is repeated across multiple tests (also at lines 128, 166).

Consider extracting a helper function to reduce duplication:

constexpectCallsBasedOnRQMode=async(spy: any,rqCalls: number,nonRqCalls: number)=>{constisRQ=Boolean((globalThisasany).__CLERK_USE_RQ__);if(isRQ){awaitwaitFor(()=>expect(spy.mock.calls.length).toBeGreaterThanOrEqual(rqCalls));}else{awaitwaitFor(()=>expect(spy).toHaveBeenCalledTimes(nonRqCalls));}};

Then use it as:

awaitexpectCallsBasedOnRQMode(getAllSpy,2,1);

177-210: Simplify the subject verification assertion.

Line 209's assertion expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true) is unnecessarily complex.

Apply this diff to make the assertion clearer:

 expect(getAllSpy).toHaveBeenCalledTimes(1);
const subjects = getAllSpy.mock.calls.map(call => (call[0] as { subject?: string })?.subject);
expect(subjects).not.toContain('user_secondary');
- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(subjects[0]).toMatch(/^(user_primary)?$/);

Or even more explicitly:

- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(['user_primary', undefined]).toContain(subjects[0]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd87133 and a973f80.

📒 Files selected for processing (2)
  • .changeset/young-impalas-grab.md (1 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
.changeset/young-impalas-grab.md (1)

1-14: LGTM! Clear documentation of the behavior change.

The changeset clearly describes the relaxed revalidation behavior, and the code example effectively demonstrates how revalidation now works across different hook configurations.

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

32-38: LGTM! Proper test isolation.

The beforeEach hook correctly clears mocks and the query client, ensuring each test starts with a clean state.


40-62: LGTM! Clear revalidation test.

This test effectively verifies that calling revalidate() fetches fresh data from the API.


1-211: Excellent test coverage for revalidation behavior.

The test suite comprehensively covers:

  • Basic revalidation functionality
  • Cascading revalidation across paginated and infinite modes
  • Configuration variations (pageSize, query filters)
  • Subject isolation (negative test)
  • Behavior differences based on the RQ feature flag

All async operations are properly handled with act() and waitFor(), and test isolation is maintained through proper cleanup.

@panteliselef
panteliselef merged commit 00291bc into mainNov 17, 2025
45 checks passed
@panteliselef
panteliselef deleted the elef/improve-revalidations-in-rq-variants branch November 17, 2025 13:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@panteliselef@octoper
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(shared): Lax revalidation for RQ variant hooks - #7228

Merged
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants
Nov 17, 2025
Merged

feat(shared): Lax revalidation for RQ variant hooks#7228
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants

Conversation

@panteliselef

@panteliselefpanteliselef commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Description

The revalidate() function returned from the new RQ variant hooks will revalidate based on a provided invalidationKey and it will revalidate pagination and infinite mode hooks that match with it.

This changes comes as new "sane" default as this ensures that data will be revalidated across different configuration between the host application and the AIOs.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Breaking Changes

    • Revalidation is now asynchronous and returns a promise.
    • The shape of invalidation keys changed; callers relying on the previous key format must update.
    • Revalidation matching rules relaxed so variant hooks can trigger each other's revalidation.
  • Tests

    • Added extensive tests for revalidation behavior across paginated/infinite hooks, cascading, isolation, and various configurations.

@changeset-bot

changeset-botBot commented Nov 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 415d329

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
NameType
@clerk/sharedPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/chrome-extensionPatch
@clerk/clerk-jsPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/clerk-expoPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/themesPatch
@clerk/typesPatch
@clerk/vuePatch
@clerk/localizationsPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Nov 15, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewCommentNov 17, 2025 0:37am

@coderabbitai

coderabbitaiBot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds extensive revalidation tests across multiple hooks, introduces an InvalidationQueryKey type replacing prior key types, and changes usePagesOrInfinite.rq.tsx's revalidate to async with two sequential cache invalidations using the generalized invalidation key.

Changes

Cohort / File(s)Summary
Revalidation Test Suites
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx, packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts, packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx, packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx
Added tests invoking revalidate, asserting refetches, cache updates, and conditional cascades based on __CLERK_USE_RQ__. Imported act where required.
useAPIKeys Hook Tests
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx
New comprehensive test suite covering initial fetch, revalidation, cascade behavior across paginated/infinite modes, page sizes, filters, and subject isolation.
Pages/Infinite Types
packages/shared/src/react/hooks/usePageOrInfinite.types.ts
Removed Register and AnyQueryKey. Added InvalidationQueryKey = readonly [string, boolean, Record<string, unknown>]. Updated invalidationKey in UsePagesOrInfiniteSignature to use InvalidationQueryKey.
React Query Implementation
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx
Converted revalidate from sync to async; now awaits invalidateQueries(keys.invalidationKey), derives a stable prefix plus -inf variant, then awaits a second invalidateQueries call and returns its result.
Changeset
.changeset/young-impalas-grab.md
Adds changeset describing relaxed revalidation behavior across hook configurations for a patch release of the package.

Sequence Diagram

sequenceDiagram
participant Caller
participant Hook as usePagesOrInfinite.revalidate()
participant QC as QueryClient
rect rgb(245,245,255)
Note over Hook: OLD (sync) - choose one variant to invalidate
Caller->>Hook: revalidate()
alt was infinite
Hook->>QC: invalidateQueries(infiniteQueryKey)
else
Hook->>QC: invalidateQueries(pagesQueryKey)
end
QC-->>Hook: done
Hook-->>Caller: done
end
rect rgb(245,255,245)
Note over Hook: NEW (async) - generalized invalidationKey → sequential invalidations
Caller->>Hook: revalidate()
Hook->>QC: await invalidateQueries(keys.invalidationKey)
QC-->>Hook: resultA
Hook->>Hook: derive stablePrefix + '-inf' + rest
Hook->>QC: await invalidateQueries(derivedInfKey)
QC-->>Hook: resultB
Hook-->>Caller: Promise(resultB)
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect:
    • usePagesOrInfinite.rq.tsx — key-splitting, derived key formation, and return value semantics.
    • usePageOrInfinite.types.ts — consumers adapting to InvalidationQueryKey.
    • New tests — conditional assertions tied to __CLERK_USE_RQ__, proper use of act, and async waits.

Poem

🐰 I nudged two keys to wake the store,

One then the other — I peered for more.
Pages and infinite, both in chase,
Fresh data tumbled into place,
I thumped my foot — the queries race.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: making revalidation behavior more permissive for React Query variant hooks.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elef/improve-revalidations-in-rq-variants

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a973f80 and 415d329.

📒 Files selected for processing (1)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (6)

1-30: LGTM! Clean test setup.

The mock structure and module setup follow testing best practices. The explicit type annotation for getAllSpy enhances readability.


32-38: LGTM! Proper test isolation.

The beforeEach hook correctly resets mocks and query client state, ensuring each test starts with a clean slate.


40-62: LGTM! Solid basic revalidation test.

The test properly handles async operations with act and waitFor, and clearly validates the revalidation flow from initial data to updated data.


101-175: Verify mock parameter names for pageSize and query.

Similar to the previous test, these mocks use parameter names like { pageSize } (line 103) and { query } (line 141) that need to match the actual API signature to ensure the mocks receive the expected values during testing.

The verification script from the previous comment will also check these parameter names.


177-212: LGTM! Subject isolation test validates proper cascade scoping.

This test correctly verifies that revalidation does not cascade across different subjects, which is important for ensuring data isolation. The type assertion on lines 207-211, while complex, safely handles the mock call structure.

However, verify that the { subject } parameter name on line 179 matches the actual API signature, as with the other tests.


64-99: The mock implementation correctly uses valid API parameters.

The test mock at line 66 correctly destructures initialPage from the params object. This parameter is defined in ClerkPaginationParams (which GetAPIKeysParams extends), alongside pageSize. The actual GetAPIKeysParams type expands to include initialPage, pageSize, subject, and query—all of which are properly used across the test cases in this file.

The concern raised in the original review comment does not apply; the mock parameter names match the actual API signature.

Likely an incorrect or invalid review comment.


Comment @coderabbitai help to get the list of available commands and usage tips.

@panteliselefpanteliselef self-assigned this Nov 15, 2025
@panteliselef
panteliselef requested review from a teamNovember 15, 2025 15:11
Base automatically changed from elef/improve-cache-key-construction to mainNovember 17, 2025 11:43
# Conflicts:
#	packages/shared/src/react/hooks/useAPIKeys.ts
#	packages/shared/src/react/hooks/usePageOrInfinite.types.ts
@pkg-pr-new

pkg-pr-newBot commented Nov 17, 2025

Copy link
Copy Markdown

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7228

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7228

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7228

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7228

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7228

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7228

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7228

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7228

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7228

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7228

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7228

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7228

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7228

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7228

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7228

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7228

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7228

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7228

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7228

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7228

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7228

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7228

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7228

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7228

commit: 415d329

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (1)

1-1: Plan revalidation tests nicely pin down cache and isolation behavior

  • "revalidate refetches plans and updates cache" confirms the hook uses the updated revalidate semantics to refresh data and update the cached page.
  • "revalidate for user plans does not refetch organization plans" effectively asserts that invalidation is scoped by the for dimension, so org plans aren’t accidentally refetched when user plans are revalidated. The isRQ gating and calls.every(value => value === 'user') check express this clearly.

Together these tests give good confidence that user vs organization plan queries won’t interfere with each other while still benefiting from the generalized invalidation.

You could optionally also assert that at least one revalidation call has for: 'user' even in the RQ branch (using waitFor on both length and argument shape) to strengthen the assertion, but it’s not strictly necessary.

Also applies to: 207-278

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1)

1-211: Comprehensive revalidation coverage for useApiKeys

This new suite thoroughly exercises useApiKeys revalidation:

  • Verifies that revalidate refreshes data and increments the underlying getAll call count.
  • Confirms cascade behavior between paginated and infinite variants only in RQ mode via __CLERK_USE_RQ__.
  • Checks that cascades respect configuration boundaries (different pageSize, query, and especially different subject values).

The subject-isolation test in particular is a nice guardrail against over-broad invalidation. Overall the structure and use of act/waitFor look solid.

You might optionally tighten the “different pageSize” and “different query filters” tests by asserting that, in the RQ branch, at least one revalidation call corresponds to each configuration (e.g., checking pageSize / query values in getAllSpy.mock.calls) to more directly verify that both related queries are refetched.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4e7a7 and bd87133.

📒 Files selected for processing (7)
  • packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
  • packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2 hunks)
  • packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1 hunks)
  • packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)
packages/clerk-js/src/test/utils.ts (3)
  • renderHook (77-77)
  • waitFor (73-73)
  • act (75-75)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2)
packages/shared/src/types/billing.ts (1)
  • BillingPlanResource (120-187)
packages/shared/src/react/hooks/usePlans.tsx (1)
  • usePlans (8-21)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2)
packages/shared/src/react/hooks/useSubscription.rq.tsx (1)
  • useSubscription (43-99)
packages/shared/src/react/hooks/useSubscription.swr.tsx (1)
  • useSubscription (22-71)
packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)
  • usePagesOrInfinite (21-291)
packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx (1)
  • usePagesOrInfinite (37-237)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (1)

1-1: Revalidation test for subscriptions is well-structured

Importing act and the new "revalidate fetches the latest subscription data" test correctly exercise the hook’s revalidate contract (including async behavior) and mirror patterns used elsewhere in this suite. This should protect against regressions in both RQ and SWR variants.

Also applies to: 217-233

packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)

505-533: Good coverage of basic revalidate refetch semantics

The "refetches current data when revalidate is invoked" test cleanly validates that revalidate performs a fresh fetch and updates data, using act + waitFor in an async-friendly way. This aligns with the new async revalidate implementation and should remain stable across RQ/SWR implementations.


684-724: Cascade revalidation expectations are clearly specified

The "cascades revalidation to related queries only in React Query mode" test nicely captures the intended behavior: in RQ mode, invalidating via the paginated hook also refetches the infinite counterpart, while non-RQ mode keeps revalidation local. The __CLERK_USE_RQ__ gating plus lenient >= 2 assertion make this resilient to minor fetch scheduling differences.

packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)

1-1: Revalidation behavior for billing hooks is well-validated

The added "revalidate behavior" suite does a good job of:

  • Verifying that revalidate actually refreshes paginated data.
  • Asserting that, in RQ mode, revalidation of the paginated hook also triggers the infinite variant while staying local in non-RQ mode.

This ties the higher-level createBillingPaginatedHook behavior back to the updated usePagesOrInfinite revalidation strategy without over-constraining call counts.

Also applies to: 490-550

packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1)

21-22: Invalidation key typing matches the new revalidate implementation

Defining InvalidationQueryKey as [string, boolean, Record<string, unknown>] and wiring keys.invalidationKey to that shape lines up with how usePagesOrInfinite.rq.tsx now destructures and uses the invalidation key (stable prefix, authenticated flag, tracked keys). This tighter typing should help prevent accidentally passing full query keys or mismatched structures into the hook.

It’s worth confirming that all createCacheKeys call sites (and any custom keys objects, if they exist) still satisfy this narrower invalidationKey shape once type-checking and the test suite run.

Also applies to: 28-28

packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)

268-271: Async revalidate with generalized invalidation key looks correct

The new revalidate implementation:

  • First invalidates keys.invalidationKey, which matches the paginated query key prefix.
  • Then derives [stablePrefix + '-inf', ...rest] to invalidate the corresponding infinite queries by prefix.
  • Returns the second invalidation promise, making revalidate awaitable in tests and consumers.

This matches the intended “cascade across pagination/infinite variants sharing the same invalidation key” behavior and is consistent with how tests assert fan-out in RQ mode.

Please ensure this aligns with the exact invalidateQueries prefix-matching semantics in your TanStack Query version (v5 APIs), and that no call sites relied on revalidate being strictly synchronous; the tests added in this PR already cover the new async contract, but a quick type-check / test run will confirm.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

8-14: Consider a more specific type assertion for the mock return value.

The Record<string, unknown> type for data array items is quite loose and could mask type mismatches between the mock and actual API response.

Consider defining a more specific type for the mock API key data, for example:

+type MockAPIKey = { id: string; [key: string]: unknown };+
const getAllSpy = vi.fn(
async () =>
({
data: [],
total_count: 0,
- }) as { data: Array<Record<string, unknown>>; total_count: number },+ }) as { data: Array<MockAPIKey>; total_count: number },
);

64-99: Consider extracting the RQ flag check pattern.

The pattern of checking (globalThis as any).__CLERK_USE_RQ__ and conditionally asserting call counts is repeated across multiple tests (also at lines 128, 166).

Consider extracting a helper function to reduce duplication:

constexpectCallsBasedOnRQMode=async(spy: any,rqCalls: number,nonRqCalls: number)=>{constisRQ=Boolean((globalThisasany).__CLERK_USE_RQ__);if(isRQ){awaitwaitFor(()=>expect(spy.mock.calls.length).toBeGreaterThanOrEqual(rqCalls));}else{awaitwaitFor(()=>expect(spy).toHaveBeenCalledTimes(nonRqCalls));}};

Then use it as:

awaitexpectCallsBasedOnRQMode(getAllSpy,2,1);

177-210: Simplify the subject verification assertion.

Line 209's assertion expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true) is unnecessarily complex.

Apply this diff to make the assertion clearer:

 expect(getAllSpy).toHaveBeenCalledTimes(1);
const subjects = getAllSpy.mock.calls.map(call => (call[0] as { subject?: string })?.subject);
expect(subjects).not.toContain('user_secondary');
- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(subjects[0]).toMatch(/^(user_primary)?$/);

Or even more explicitly:

- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(['user_primary', undefined]).toContain(subjects[0]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd87133 and a973f80.

📒 Files selected for processing (2)
  • .changeset/young-impalas-grab.md (1 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
.changeset/young-impalas-grab.md (1)

1-14: LGTM! Clear documentation of the behavior change.

The changeset clearly describes the relaxed revalidation behavior, and the code example effectively demonstrates how revalidation now works across different hook configurations.

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

32-38: LGTM! Proper test isolation.

The beforeEach hook correctly clears mocks and the query client, ensuring each test starts with a clean state.


40-62: LGTM! Clear revalidation test.

This test effectively verifies that calling revalidate() fetches fresh data from the API.


1-211: Excellent test coverage for revalidation behavior.

The test suite comprehensively covers:

  • Basic revalidation functionality
  • Cascading revalidation across paginated and infinite modes
  • Configuration variations (pageSize, query filters)
  • Subject isolation (negative test)
  • Behavior differences based on the RQ feature flag

All async operations are properly handled with act() and waitFor(), and test isolation is maintained through proper cleanup.

@panteliselef
panteliselef merged commit 00291bc into mainNov 17, 2025
45 checks passed
@panteliselef
panteliselef deleted the elef/improve-revalidations-in-rq-variants branch November 17, 2025 13:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@panteliselef@octoper
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(shared): Lax revalidation for RQ variant hooks - #7228

Merged
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants
Nov 17, 2025
Merged

feat(shared): Lax revalidation for RQ variant hooks#7228
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants

Conversation

@panteliselef

@panteliselefpanteliselef commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Description

The revalidate() function returned from the new RQ variant hooks will revalidate based on a provided invalidationKey and it will revalidate pagination and infinite mode hooks that match with it.

This changes comes as new "sane" default as this ensures that data will be revalidated across different configuration between the host application and the AIOs.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Breaking Changes

    • Revalidation is now asynchronous and returns a promise.
    • The shape of invalidation keys changed; callers relying on the previous key format must update.
    • Revalidation matching rules relaxed so variant hooks can trigger each other's revalidation.
  • Tests

    • Added extensive tests for revalidation behavior across paginated/infinite hooks, cascading, isolation, and various configurations.

@changeset-bot

changeset-botBot commented Nov 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 415d329

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
NameType
@clerk/sharedPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/chrome-extensionPatch
@clerk/clerk-jsPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/clerk-expoPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/themesPatch
@clerk/typesPatch
@clerk/vuePatch
@clerk/localizationsPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Nov 15, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewCommentNov 17, 2025 0:37am

@coderabbitai

coderabbitaiBot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds extensive revalidation tests across multiple hooks, introduces an InvalidationQueryKey type replacing prior key types, and changes usePagesOrInfinite.rq.tsx's revalidate to async with two sequential cache invalidations using the generalized invalidation key.

Changes

Cohort / File(s)Summary
Revalidation Test Suites
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx, packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts, packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx, packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx
Added tests invoking revalidate, asserting refetches, cache updates, and conditional cascades based on __CLERK_USE_RQ__. Imported act where required.
useAPIKeys Hook Tests
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx
New comprehensive test suite covering initial fetch, revalidation, cascade behavior across paginated/infinite modes, page sizes, filters, and subject isolation.
Pages/Infinite Types
packages/shared/src/react/hooks/usePageOrInfinite.types.ts
Removed Register and AnyQueryKey. Added InvalidationQueryKey = readonly [string, boolean, Record<string, unknown>]. Updated invalidationKey in UsePagesOrInfiniteSignature to use InvalidationQueryKey.
React Query Implementation
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx
Converted revalidate from sync to async; now awaits invalidateQueries(keys.invalidationKey), derives a stable prefix plus -inf variant, then awaits a second invalidateQueries call and returns its result.
Changeset
.changeset/young-impalas-grab.md
Adds changeset describing relaxed revalidation behavior across hook configurations for a patch release of the package.

Sequence Diagram

sequenceDiagram
participant Caller
participant Hook as usePagesOrInfinite.revalidate()
participant QC as QueryClient
rect rgb(245,245,255)
Note over Hook: OLD (sync) - choose one variant to invalidate
Caller->>Hook: revalidate()
alt was infinite
Hook->>QC: invalidateQueries(infiniteQueryKey)
else
Hook->>QC: invalidateQueries(pagesQueryKey)
end
QC-->>Hook: done
Hook-->>Caller: done
end
rect rgb(245,255,245)
Note over Hook: NEW (async) - generalized invalidationKey → sequential invalidations
Caller->>Hook: revalidate()
Hook->>QC: await invalidateQueries(keys.invalidationKey)
QC-->>Hook: resultA
Hook->>Hook: derive stablePrefix + '-inf' + rest
Hook->>QC: await invalidateQueries(derivedInfKey)
QC-->>Hook: resultB
Hook-->>Caller: Promise(resultB)
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect:
    • usePagesOrInfinite.rq.tsx — key-splitting, derived key formation, and return value semantics.
    • usePageOrInfinite.types.ts — consumers adapting to InvalidationQueryKey.
    • New tests — conditional assertions tied to __CLERK_USE_RQ__, proper use of act, and async waits.

Poem

🐰 I nudged two keys to wake the store,

One then the other — I peered for more.
Pages and infinite, both in chase,
Fresh data tumbled into place,
I thumped my foot — the queries race.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: making revalidation behavior more permissive for React Query variant hooks.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elef/improve-revalidations-in-rq-variants

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a973f80 and 415d329.

📒 Files selected for processing (1)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (6)

1-30: LGTM! Clean test setup.

The mock structure and module setup follow testing best practices. The explicit type annotation for getAllSpy enhances readability.


32-38: LGTM! Proper test isolation.

The beforeEach hook correctly resets mocks and query client state, ensuring each test starts with a clean slate.


40-62: LGTM! Solid basic revalidation test.

The test properly handles async operations with act and waitFor, and clearly validates the revalidation flow from initial data to updated data.


101-175: Verify mock parameter names for pageSize and query.

Similar to the previous test, these mocks use parameter names like { pageSize } (line 103) and { query } (line 141) that need to match the actual API signature to ensure the mocks receive the expected values during testing.

The verification script from the previous comment will also check these parameter names.


177-212: LGTM! Subject isolation test validates proper cascade scoping.

This test correctly verifies that revalidation does not cascade across different subjects, which is important for ensuring data isolation. The type assertion on lines 207-211, while complex, safely handles the mock call structure.

However, verify that the { subject } parameter name on line 179 matches the actual API signature, as with the other tests.


64-99: The mock implementation correctly uses valid API parameters.

The test mock at line 66 correctly destructures initialPage from the params object. This parameter is defined in ClerkPaginationParams (which GetAPIKeysParams extends), alongside pageSize. The actual GetAPIKeysParams type expands to include initialPage, pageSize, subject, and query—all of which are properly used across the test cases in this file.

The concern raised in the original review comment does not apply; the mock parameter names match the actual API signature.

Likely an incorrect or invalid review comment.


Comment @coderabbitai help to get the list of available commands and usage tips.

@panteliselefpanteliselef self-assigned this Nov 15, 2025
@panteliselef
panteliselef requested review from a teamNovember 15, 2025 15:11
Base automatically changed from elef/improve-cache-key-construction to mainNovember 17, 2025 11:43
# Conflicts:
#	packages/shared/src/react/hooks/useAPIKeys.ts
#	packages/shared/src/react/hooks/usePageOrInfinite.types.ts
@pkg-pr-new

pkg-pr-newBot commented Nov 17, 2025

Copy link
Copy Markdown

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7228

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7228

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7228

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7228

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7228

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7228

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7228

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7228

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7228

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7228

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7228

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7228

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7228

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7228

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7228

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7228

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7228

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7228

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7228

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7228

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7228

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7228

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7228

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7228

commit: 415d329

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (1)

1-1: Plan revalidation tests nicely pin down cache and isolation behavior

  • "revalidate refetches plans and updates cache" confirms the hook uses the updated revalidate semantics to refresh data and update the cached page.
  • "revalidate for user plans does not refetch organization plans" effectively asserts that invalidation is scoped by the for dimension, so org plans aren’t accidentally refetched when user plans are revalidated. The isRQ gating and calls.every(value => value === 'user') check express this clearly.

Together these tests give good confidence that user vs organization plan queries won’t interfere with each other while still benefiting from the generalized invalidation.

You could optionally also assert that at least one revalidation call has for: 'user' even in the RQ branch (using waitFor on both length and argument shape) to strengthen the assertion, but it’s not strictly necessary.

Also applies to: 207-278

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1)

1-211: Comprehensive revalidation coverage for useApiKeys

This new suite thoroughly exercises useApiKeys revalidation:

  • Verifies that revalidate refreshes data and increments the underlying getAll call count.
  • Confirms cascade behavior between paginated and infinite variants only in RQ mode via __CLERK_USE_RQ__.
  • Checks that cascades respect configuration boundaries (different pageSize, query, and especially different subject values).

The subject-isolation test in particular is a nice guardrail against over-broad invalidation. Overall the structure and use of act/waitFor look solid.

You might optionally tighten the “different pageSize” and “different query filters” tests by asserting that, in the RQ branch, at least one revalidation call corresponds to each configuration (e.g., checking pageSize / query values in getAllSpy.mock.calls) to more directly verify that both related queries are refetched.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4e7a7 and bd87133.

📒 Files selected for processing (7)
  • packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
  • packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2 hunks)
  • packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1 hunks)
  • packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)
packages/clerk-js/src/test/utils.ts (3)
  • renderHook (77-77)
  • waitFor (73-73)
  • act (75-75)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2)
packages/shared/src/types/billing.ts (1)
  • BillingPlanResource (120-187)
packages/shared/src/react/hooks/usePlans.tsx (1)
  • usePlans (8-21)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2)
packages/shared/src/react/hooks/useSubscription.rq.tsx (1)
  • useSubscription (43-99)
packages/shared/src/react/hooks/useSubscription.swr.tsx (1)
  • useSubscription (22-71)
packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)
  • usePagesOrInfinite (21-291)
packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx (1)
  • usePagesOrInfinite (37-237)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (1)

1-1: Revalidation test for subscriptions is well-structured

Importing act and the new "revalidate fetches the latest subscription data" test correctly exercise the hook’s revalidate contract (including async behavior) and mirror patterns used elsewhere in this suite. This should protect against regressions in both RQ and SWR variants.

Also applies to: 217-233

packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)

505-533: Good coverage of basic revalidate refetch semantics

The "refetches current data when revalidate is invoked" test cleanly validates that revalidate performs a fresh fetch and updates data, using act + waitFor in an async-friendly way. This aligns with the new async revalidate implementation and should remain stable across RQ/SWR implementations.


684-724: Cascade revalidation expectations are clearly specified

The "cascades revalidation to related queries only in React Query mode" test nicely captures the intended behavior: in RQ mode, invalidating via the paginated hook also refetches the infinite counterpart, while non-RQ mode keeps revalidation local. The __CLERK_USE_RQ__ gating plus lenient >= 2 assertion make this resilient to minor fetch scheduling differences.

packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)

1-1: Revalidation behavior for billing hooks is well-validated

The added "revalidate behavior" suite does a good job of:

  • Verifying that revalidate actually refreshes paginated data.
  • Asserting that, in RQ mode, revalidation of the paginated hook also triggers the infinite variant while staying local in non-RQ mode.

This ties the higher-level createBillingPaginatedHook behavior back to the updated usePagesOrInfinite revalidation strategy without over-constraining call counts.

Also applies to: 490-550

packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1)

21-22: Invalidation key typing matches the new revalidate implementation

Defining InvalidationQueryKey as [string, boolean, Record<string, unknown>] and wiring keys.invalidationKey to that shape lines up with how usePagesOrInfinite.rq.tsx now destructures and uses the invalidation key (stable prefix, authenticated flag, tracked keys). This tighter typing should help prevent accidentally passing full query keys or mismatched structures into the hook.

It’s worth confirming that all createCacheKeys call sites (and any custom keys objects, if they exist) still satisfy this narrower invalidationKey shape once type-checking and the test suite run.

Also applies to: 28-28

packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)

268-271: Async revalidate with generalized invalidation key looks correct

The new revalidate implementation:

  • First invalidates keys.invalidationKey, which matches the paginated query key prefix.
  • Then derives [stablePrefix + '-inf', ...rest] to invalidate the corresponding infinite queries by prefix.
  • Returns the second invalidation promise, making revalidate awaitable in tests and consumers.

This matches the intended “cascade across pagination/infinite variants sharing the same invalidation key” behavior and is consistent with how tests assert fan-out in RQ mode.

Please ensure this aligns with the exact invalidateQueries prefix-matching semantics in your TanStack Query version (v5 APIs), and that no call sites relied on revalidate being strictly synchronous; the tests added in this PR already cover the new async contract, but a quick type-check / test run will confirm.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

8-14: Consider a more specific type assertion for the mock return value.

The Record<string, unknown> type for data array items is quite loose and could mask type mismatches between the mock and actual API response.

Consider defining a more specific type for the mock API key data, for example:

+type MockAPIKey = { id: string; [key: string]: unknown };+
const getAllSpy = vi.fn(
async () =>
({
data: [],
total_count: 0,
- }) as { data: Array<Record<string, unknown>>; total_count: number },+ }) as { data: Array<MockAPIKey>; total_count: number },
);

64-99: Consider extracting the RQ flag check pattern.

The pattern of checking (globalThis as any).__CLERK_USE_RQ__ and conditionally asserting call counts is repeated across multiple tests (also at lines 128, 166).

Consider extracting a helper function to reduce duplication:

constexpectCallsBasedOnRQMode=async(spy: any,rqCalls: number,nonRqCalls: number)=>{constisRQ=Boolean((globalThisasany).__CLERK_USE_RQ__);if(isRQ){awaitwaitFor(()=>expect(spy.mock.calls.length).toBeGreaterThanOrEqual(rqCalls));}else{awaitwaitFor(()=>expect(spy).toHaveBeenCalledTimes(nonRqCalls));}};

Then use it as:

awaitexpectCallsBasedOnRQMode(getAllSpy,2,1);

177-210: Simplify the subject verification assertion.

Line 209's assertion expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true) is unnecessarily complex.

Apply this diff to make the assertion clearer:

 expect(getAllSpy).toHaveBeenCalledTimes(1);
const subjects = getAllSpy.mock.calls.map(call => (call[0] as { subject?: string })?.subject);
expect(subjects).not.toContain('user_secondary');
- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(subjects[0]).toMatch(/^(user_primary)?$/);

Or even more explicitly:

- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(['user_primary', undefined]).toContain(subjects[0]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd87133 and a973f80.

📒 Files selected for processing (2)
  • .changeset/young-impalas-grab.md (1 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
.changeset/young-impalas-grab.md (1)

1-14: LGTM! Clear documentation of the behavior change.

The changeset clearly describes the relaxed revalidation behavior, and the code example effectively demonstrates how revalidation now works across different hook configurations.

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

32-38: LGTM! Proper test isolation.

The beforeEach hook correctly clears mocks and the query client, ensuring each test starts with a clean state.


40-62: LGTM! Clear revalidation test.

This test effectively verifies that calling revalidate() fetches fresh data from the API.


1-211: Excellent test coverage for revalidation behavior.

The test suite comprehensively covers:

  • Basic revalidation functionality
  • Cascading revalidation across paginated and infinite modes
  • Configuration variations (pageSize, query filters)
  • Subject isolation (negative test)
  • Behavior differences based on the RQ feature flag

All async operations are properly handled with act() and waitFor(), and test isolation is maintained through proper cleanup.

@panteliselef
panteliselef merged commit 00291bc into mainNov 17, 2025
45 checks passed
@panteliselef
panteliselef deleted the elef/improve-revalidations-in-rq-variants branch November 17, 2025 13:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@panteliselef@octoper
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(shared): Lax revalidation for RQ variant hooks - #7228

Merged
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants
Nov 17, 2025
Merged

feat(shared): Lax revalidation for RQ variant hooks#7228
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants

Conversation

@panteliselef

@panteliselefpanteliselef commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Description

The revalidate() function returned from the new RQ variant hooks will revalidate based on a provided invalidationKey and it will revalidate pagination and infinite mode hooks that match with it.

This changes comes as new "sane" default as this ensures that data will be revalidated across different configuration between the host application and the AIOs.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Breaking Changes

    • Revalidation is now asynchronous and returns a promise.
    • The shape of invalidation keys changed; callers relying on the previous key format must update.
    • Revalidation matching rules relaxed so variant hooks can trigger each other's revalidation.
  • Tests

    • Added extensive tests for revalidation behavior across paginated/infinite hooks, cascading, isolation, and various configurations.

@changeset-bot

changeset-botBot commented Nov 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 415d329

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
NameType
@clerk/sharedPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/chrome-extensionPatch
@clerk/clerk-jsPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/clerk-expoPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/themesPatch
@clerk/typesPatch
@clerk/vuePatch
@clerk/localizationsPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Nov 15, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewCommentNov 17, 2025 0:37am

@coderabbitai

coderabbitaiBot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds extensive revalidation tests across multiple hooks, introduces an InvalidationQueryKey type replacing prior key types, and changes usePagesOrInfinite.rq.tsx's revalidate to async with two sequential cache invalidations using the generalized invalidation key.

Changes

Cohort / File(s)Summary
Revalidation Test Suites
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx, packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts, packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx, packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx
Added tests invoking revalidate, asserting refetches, cache updates, and conditional cascades based on __CLERK_USE_RQ__. Imported act where required.
useAPIKeys Hook Tests
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx
New comprehensive test suite covering initial fetch, revalidation, cascade behavior across paginated/infinite modes, page sizes, filters, and subject isolation.
Pages/Infinite Types
packages/shared/src/react/hooks/usePageOrInfinite.types.ts
Removed Register and AnyQueryKey. Added InvalidationQueryKey = readonly [string, boolean, Record<string, unknown>]. Updated invalidationKey in UsePagesOrInfiniteSignature to use InvalidationQueryKey.
React Query Implementation
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx
Converted revalidate from sync to async; now awaits invalidateQueries(keys.invalidationKey), derives a stable prefix plus -inf variant, then awaits a second invalidateQueries call and returns its result.
Changeset
.changeset/young-impalas-grab.md
Adds changeset describing relaxed revalidation behavior across hook configurations for a patch release of the package.

Sequence Diagram

sequenceDiagram
participant Caller
participant Hook as usePagesOrInfinite.revalidate()
participant QC as QueryClient
rect rgb(245,245,255)
Note over Hook: OLD (sync) - choose one variant to invalidate
Caller->>Hook: revalidate()
alt was infinite
Hook->>QC: invalidateQueries(infiniteQueryKey)
else
Hook->>QC: invalidateQueries(pagesQueryKey)
end
QC-->>Hook: done
Hook-->>Caller: done
end
rect rgb(245,255,245)
Note over Hook: NEW (async) - generalized invalidationKey → sequential invalidations
Caller->>Hook: revalidate()
Hook->>QC: await invalidateQueries(keys.invalidationKey)
QC-->>Hook: resultA
Hook->>Hook: derive stablePrefix + '-inf' + rest
Hook->>QC: await invalidateQueries(derivedInfKey)
QC-->>Hook: resultB
Hook-->>Caller: Promise(resultB)
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect:
    • usePagesOrInfinite.rq.tsx — key-splitting, derived key formation, and return value semantics.
    • usePageOrInfinite.types.ts — consumers adapting to InvalidationQueryKey.
    • New tests — conditional assertions tied to __CLERK_USE_RQ__, proper use of act, and async waits.

Poem

🐰 I nudged two keys to wake the store,

One then the other — I peered for more.
Pages and infinite, both in chase,
Fresh data tumbled into place,
I thumped my foot — the queries race.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: making revalidation behavior more permissive for React Query variant hooks.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elef/improve-revalidations-in-rq-variants

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a973f80 and 415d329.

📒 Files selected for processing (1)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (6)

1-30: LGTM! Clean test setup.

The mock structure and module setup follow testing best practices. The explicit type annotation for getAllSpy enhances readability.


32-38: LGTM! Proper test isolation.

The beforeEach hook correctly resets mocks and query client state, ensuring each test starts with a clean slate.


40-62: LGTM! Solid basic revalidation test.

The test properly handles async operations with act and waitFor, and clearly validates the revalidation flow from initial data to updated data.


101-175: Verify mock parameter names for pageSize and query.

Similar to the previous test, these mocks use parameter names like { pageSize } (line 103) and { query } (line 141) that need to match the actual API signature to ensure the mocks receive the expected values during testing.

The verification script from the previous comment will also check these parameter names.


177-212: LGTM! Subject isolation test validates proper cascade scoping.

This test correctly verifies that revalidation does not cascade across different subjects, which is important for ensuring data isolation. The type assertion on lines 207-211, while complex, safely handles the mock call structure.

However, verify that the { subject } parameter name on line 179 matches the actual API signature, as with the other tests.


64-99: The mock implementation correctly uses valid API parameters.

The test mock at line 66 correctly destructures initialPage from the params object. This parameter is defined in ClerkPaginationParams (which GetAPIKeysParams extends), alongside pageSize. The actual GetAPIKeysParams type expands to include initialPage, pageSize, subject, and query—all of which are properly used across the test cases in this file.

The concern raised in the original review comment does not apply; the mock parameter names match the actual API signature.

Likely an incorrect or invalid review comment.


Comment @coderabbitai help to get the list of available commands and usage tips.

@panteliselefpanteliselef self-assigned this Nov 15, 2025
@panteliselef
panteliselef requested review from a teamNovember 15, 2025 15:11
Base automatically changed from elef/improve-cache-key-construction to mainNovember 17, 2025 11:43
# Conflicts:
#	packages/shared/src/react/hooks/useAPIKeys.ts
#	packages/shared/src/react/hooks/usePageOrInfinite.types.ts
@pkg-pr-new

pkg-pr-newBot commented Nov 17, 2025

Copy link
Copy Markdown

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7228

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7228

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7228

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7228

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7228

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7228

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7228

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7228

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7228

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7228

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7228

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7228

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7228

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7228

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7228

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7228

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7228

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7228

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7228

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7228

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7228

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7228

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7228

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7228

commit: 415d329

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (1)

1-1: Plan revalidation tests nicely pin down cache and isolation behavior

  • "revalidate refetches plans and updates cache" confirms the hook uses the updated revalidate semantics to refresh data and update the cached page.
  • "revalidate for user plans does not refetch organization plans" effectively asserts that invalidation is scoped by the for dimension, so org plans aren’t accidentally refetched when user plans are revalidated. The isRQ gating and calls.every(value => value === 'user') check express this clearly.

Together these tests give good confidence that user vs organization plan queries won’t interfere with each other while still benefiting from the generalized invalidation.

You could optionally also assert that at least one revalidation call has for: 'user' even in the RQ branch (using waitFor on both length and argument shape) to strengthen the assertion, but it’s not strictly necessary.

Also applies to: 207-278

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1)

1-211: Comprehensive revalidation coverage for useApiKeys

This new suite thoroughly exercises useApiKeys revalidation:

  • Verifies that revalidate refreshes data and increments the underlying getAll call count.
  • Confirms cascade behavior between paginated and infinite variants only in RQ mode via __CLERK_USE_RQ__.
  • Checks that cascades respect configuration boundaries (different pageSize, query, and especially different subject values).

The subject-isolation test in particular is a nice guardrail against over-broad invalidation. Overall the structure and use of act/waitFor look solid.

You might optionally tighten the “different pageSize” and “different query filters” tests by asserting that, in the RQ branch, at least one revalidation call corresponds to each configuration (e.g., checking pageSize / query values in getAllSpy.mock.calls) to more directly verify that both related queries are refetched.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4e7a7 and bd87133.

📒 Files selected for processing (7)
  • packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
  • packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2 hunks)
  • packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1 hunks)
  • packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)
packages/clerk-js/src/test/utils.ts (3)
  • renderHook (77-77)
  • waitFor (73-73)
  • act (75-75)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2)
packages/shared/src/types/billing.ts (1)
  • BillingPlanResource (120-187)
packages/shared/src/react/hooks/usePlans.tsx (1)
  • usePlans (8-21)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2)
packages/shared/src/react/hooks/useSubscription.rq.tsx (1)
  • useSubscription (43-99)
packages/shared/src/react/hooks/useSubscription.swr.tsx (1)
  • useSubscription (22-71)
packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)
  • usePagesOrInfinite (21-291)
packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx (1)
  • usePagesOrInfinite (37-237)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (1)

1-1: Revalidation test for subscriptions is well-structured

Importing act and the new "revalidate fetches the latest subscription data" test correctly exercise the hook’s revalidate contract (including async behavior) and mirror patterns used elsewhere in this suite. This should protect against regressions in both RQ and SWR variants.

Also applies to: 217-233

packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)

505-533: Good coverage of basic revalidate refetch semantics

The "refetches current data when revalidate is invoked" test cleanly validates that revalidate performs a fresh fetch and updates data, using act + waitFor in an async-friendly way. This aligns with the new async revalidate implementation and should remain stable across RQ/SWR implementations.


684-724: Cascade revalidation expectations are clearly specified

The "cascades revalidation to related queries only in React Query mode" test nicely captures the intended behavior: in RQ mode, invalidating via the paginated hook also refetches the infinite counterpart, while non-RQ mode keeps revalidation local. The __CLERK_USE_RQ__ gating plus lenient >= 2 assertion make this resilient to minor fetch scheduling differences.

packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)

1-1: Revalidation behavior for billing hooks is well-validated

The added "revalidate behavior" suite does a good job of:

  • Verifying that revalidate actually refreshes paginated data.
  • Asserting that, in RQ mode, revalidation of the paginated hook also triggers the infinite variant while staying local in non-RQ mode.

This ties the higher-level createBillingPaginatedHook behavior back to the updated usePagesOrInfinite revalidation strategy without over-constraining call counts.

Also applies to: 490-550

packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1)

21-22: Invalidation key typing matches the new revalidate implementation

Defining InvalidationQueryKey as [string, boolean, Record<string, unknown>] and wiring keys.invalidationKey to that shape lines up with how usePagesOrInfinite.rq.tsx now destructures and uses the invalidation key (stable prefix, authenticated flag, tracked keys). This tighter typing should help prevent accidentally passing full query keys or mismatched structures into the hook.

It’s worth confirming that all createCacheKeys call sites (and any custom keys objects, if they exist) still satisfy this narrower invalidationKey shape once type-checking and the test suite run.

Also applies to: 28-28

packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)

268-271: Async revalidate with generalized invalidation key looks correct

The new revalidate implementation:

  • First invalidates keys.invalidationKey, which matches the paginated query key prefix.
  • Then derives [stablePrefix + '-inf', ...rest] to invalidate the corresponding infinite queries by prefix.
  • Returns the second invalidation promise, making revalidate awaitable in tests and consumers.

This matches the intended “cascade across pagination/infinite variants sharing the same invalidation key” behavior and is consistent with how tests assert fan-out in RQ mode.

Please ensure this aligns with the exact invalidateQueries prefix-matching semantics in your TanStack Query version (v5 APIs), and that no call sites relied on revalidate being strictly synchronous; the tests added in this PR already cover the new async contract, but a quick type-check / test run will confirm.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

8-14: Consider a more specific type assertion for the mock return value.

The Record<string, unknown> type for data array items is quite loose and could mask type mismatches between the mock and actual API response.

Consider defining a more specific type for the mock API key data, for example:

+type MockAPIKey = { id: string; [key: string]: unknown };+
const getAllSpy = vi.fn(
async () =>
({
data: [],
total_count: 0,
- }) as { data: Array<Record<string, unknown>>; total_count: number },+ }) as { data: Array<MockAPIKey>; total_count: number },
);

64-99: Consider extracting the RQ flag check pattern.

The pattern of checking (globalThis as any).__CLERK_USE_RQ__ and conditionally asserting call counts is repeated across multiple tests (also at lines 128, 166).

Consider extracting a helper function to reduce duplication:

constexpectCallsBasedOnRQMode=async(spy: any,rqCalls: number,nonRqCalls: number)=>{constisRQ=Boolean((globalThisasany).__CLERK_USE_RQ__);if(isRQ){awaitwaitFor(()=>expect(spy.mock.calls.length).toBeGreaterThanOrEqual(rqCalls));}else{awaitwaitFor(()=>expect(spy).toHaveBeenCalledTimes(nonRqCalls));}};

Then use it as:

awaitexpectCallsBasedOnRQMode(getAllSpy,2,1);

177-210: Simplify the subject verification assertion.

Line 209's assertion expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true) is unnecessarily complex.

Apply this diff to make the assertion clearer:

 expect(getAllSpy).toHaveBeenCalledTimes(1);
const subjects = getAllSpy.mock.calls.map(call => (call[0] as { subject?: string })?.subject);
expect(subjects).not.toContain('user_secondary');
- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(subjects[0]).toMatch(/^(user_primary)?$/);

Or even more explicitly:

- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(['user_primary', undefined]).toContain(subjects[0]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd87133 and a973f80.

📒 Files selected for processing (2)
  • .changeset/young-impalas-grab.md (1 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
.changeset/young-impalas-grab.md (1)

1-14: LGTM! Clear documentation of the behavior change.

The changeset clearly describes the relaxed revalidation behavior, and the code example effectively demonstrates how revalidation now works across different hook configurations.

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

32-38: LGTM! Proper test isolation.

The beforeEach hook correctly clears mocks and the query client, ensuring each test starts with a clean state.


40-62: LGTM! Clear revalidation test.

This test effectively verifies that calling revalidate() fetches fresh data from the API.


1-211: Excellent test coverage for revalidation behavior.

The test suite comprehensively covers:

  • Basic revalidation functionality
  • Cascading revalidation across paginated and infinite modes
  • Configuration variations (pageSize, query filters)
  • Subject isolation (negative test)
  • Behavior differences based on the RQ feature flag

All async operations are properly handled with act() and waitFor(), and test isolation is maintained through proper cleanup.

@panteliselef
panteliselef merged commit 00291bc into mainNov 17, 2025
45 checks passed
@panteliselef
panteliselef deleted the elef/improve-revalidations-in-rq-variants branch November 17, 2025 13:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@panteliselef@octoper
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(shared): Lax revalidation for RQ variant hooks - #7228

Merged
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants
Nov 17, 2025
Merged

feat(shared): Lax revalidation for RQ variant hooks#7228
panteliselef merged 17 commits into
mainfrom
elef/improve-revalidations-in-rq-variants

Conversation

@panteliselef

@panteliselefpanteliselef commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Description

The revalidate() function returned from the new RQ variant hooks will revalidate based on a provided invalidationKey and it will revalidate pagination and infinite mode hooks that match with it.

This changes comes as new "sane" default as this ensures that data will be revalidated across different configuration between the host application and the AIOs.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • Breaking Changes

    • Revalidation is now asynchronous and returns a promise.
    • The shape of invalidation keys changed; callers relying on the previous key format must update.
    • Revalidation matching rules relaxed so variant hooks can trigger each other's revalidation.
  • Tests

    • Added extensive tests for revalidation behavior across paginated/infinite hooks, cascading, isolation, and various configurations.

@changeset-bot

changeset-botBot commented Nov 15, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 415d329

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
NameType
@clerk/sharedPatch
@clerk/agent-toolkitPatch
@clerk/astroPatch
@clerk/backendPatch
@clerk/chrome-extensionPatch
@clerk/clerk-jsPatch
@clerk/elementsPatch
@clerk/expo-passkeysPatch
@clerk/clerk-expoPatch
@clerk/expressPatch
@clerk/fastifyPatch
@clerk/nextjsPatch
@clerk/nuxtPatch
@clerk/react-routerPatch
@clerk/clerk-reactPatch
@clerk/remixPatch
@clerk/tanstack-react-startPatch
@clerk/testingPatch
@clerk/themesPatch
@clerk/typesPatch
@clerk/vuePatch
@clerk/localizationsPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented Nov 15, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
clerk-js-sandboxReadyReadyPreviewCommentNov 17, 2025 0:37am

@coderabbitai

coderabbitaiBot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds extensive revalidation tests across multiple hooks, introduces an InvalidationQueryKey type replacing prior key types, and changes usePagesOrInfinite.rq.tsx's revalidate to async with two sequential cache invalidations using the generalized invalidation key.

Changes

Cohort / File(s)Summary
Revalidation Test Suites
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx, packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts, packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx, packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx
Added tests invoking revalidate, asserting refetches, cache updates, and conditional cascades based on __CLERK_USE_RQ__. Imported act where required.
useAPIKeys Hook Tests
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx
New comprehensive test suite covering initial fetch, revalidation, cascade behavior across paginated/infinite modes, page sizes, filters, and subject isolation.
Pages/Infinite Types
packages/shared/src/react/hooks/usePageOrInfinite.types.ts
Removed Register and AnyQueryKey. Added InvalidationQueryKey = readonly [string, boolean, Record<string, unknown>]. Updated invalidationKey in UsePagesOrInfiniteSignature to use InvalidationQueryKey.
React Query Implementation
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx
Converted revalidate from sync to async; now awaits invalidateQueries(keys.invalidationKey), derives a stable prefix plus -inf variant, then awaits a second invalidateQueries call and returns its result.
Changeset
.changeset/young-impalas-grab.md
Adds changeset describing relaxed revalidation behavior across hook configurations for a patch release of the package.

Sequence Diagram

sequenceDiagram
participant Caller
participant Hook as usePagesOrInfinite.revalidate()
participant QC as QueryClient
rect rgb(245,245,255)
Note over Hook: OLD (sync) - choose one variant to invalidate
Caller->>Hook: revalidate()
alt was infinite
Hook->>QC: invalidateQueries(infiniteQueryKey)
else
Hook->>QC: invalidateQueries(pagesQueryKey)
end
QC-->>Hook: done
Hook-->>Caller: done
end
rect rgb(245,255,245)
Note over Hook: NEW (async) - generalized invalidationKey → sequential invalidations
Caller->>Hook: revalidate()
Hook->>QC: await invalidateQueries(keys.invalidationKey)
QC-->>Hook: resultA
Hook->>Hook: derive stablePrefix + '-inf' + rest
Hook->>QC: await invalidateQueries(derivedInfKey)
QC-->>Hook: resultB
Hook-->>Caller: Promise(resultB)
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect:
    • usePagesOrInfinite.rq.tsx — key-splitting, derived key formation, and return value semantics.
    • usePageOrInfinite.types.ts — consumers adapting to InvalidationQueryKey.
    • New tests — conditional assertions tied to __CLERK_USE_RQ__, proper use of act, and async waits.

Poem

🐰 I nudged two keys to wake the store,

One then the other — I peered for more.
Pages and infinite, both in chase,
Fresh data tumbled into place,
I thumped my foot — the queries race.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately describes the main change: making revalidation behavior more permissive for React Query variant hooks.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elef/improve-revalidations-in-rq-variants

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a973f80 and 415d329.

📒 Files selected for processing (1)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (6)

1-30: LGTM! Clean test setup.

The mock structure and module setup follow testing best practices. The explicit type annotation for getAllSpy enhances readability.


32-38: LGTM! Proper test isolation.

The beforeEach hook correctly resets mocks and query client state, ensuring each test starts with a clean slate.


40-62: LGTM! Solid basic revalidation test.

The test properly handles async operations with act and waitFor, and clearly validates the revalidation flow from initial data to updated data.


101-175: Verify mock parameter names for pageSize and query.

Similar to the previous test, these mocks use parameter names like { pageSize } (line 103) and { query } (line 141) that need to match the actual API signature to ensure the mocks receive the expected values during testing.

The verification script from the previous comment will also check these parameter names.


177-212: LGTM! Subject isolation test validates proper cascade scoping.

This test correctly verifies that revalidation does not cascade across different subjects, which is important for ensuring data isolation. The type assertion on lines 207-211, while complex, safely handles the mock call structure.

However, verify that the { subject } parameter name on line 179 matches the actual API signature, as with the other tests.


64-99: The mock implementation correctly uses valid API parameters.

The test mock at line 66 correctly destructures initialPage from the params object. This parameter is defined in ClerkPaginationParams (which GetAPIKeysParams extends), alongside pageSize. The actual GetAPIKeysParams type expands to include initialPage, pageSize, subject, and query—all of which are properly used across the test cases in this file.

The concern raised in the original review comment does not apply; the mock parameter names match the actual API signature.

Likely an incorrect or invalid review comment.


Comment @coderabbitai help to get the list of available commands and usage tips.

@panteliselefpanteliselef self-assigned this Nov 15, 2025
@panteliselef
panteliselef requested review from a teamNovember 15, 2025 15:11
Base automatically changed from elef/improve-cache-key-construction to mainNovember 17, 2025 11:43
# Conflicts:
#	packages/shared/src/react/hooks/useAPIKeys.ts
#	packages/shared/src/react/hooks/usePageOrInfinite.types.ts
@pkg-pr-new

pkg-pr-newBot commented Nov 17, 2025

Copy link
Copy Markdown

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7228

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7228

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7228

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7228

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7228

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7228

@clerk/elements

npm i https://pkg.pr.new/@clerk/elements@7228

@clerk/clerk-expo

npm i https://pkg.pr.new/@clerk/clerk-expo@7228

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7228

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7228

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7228

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7228

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7228

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7228

@clerk/clerk-react

npm i https://pkg.pr.new/@clerk/clerk-react@7228

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7228

@clerk/remix

npm i https://pkg.pr.new/@clerk/remix@7228

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7228

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7228

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7228

@clerk/themes

npm i https://pkg.pr.new/@clerk/themes@7228

@clerk/types

npm i https://pkg.pr.new/@clerk/types@7228

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7228

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7228

commit: 415d329

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (1)

1-1: Plan revalidation tests nicely pin down cache and isolation behavior

  • "revalidate refetches plans and updates cache" confirms the hook uses the updated revalidate semantics to refresh data and update the cached page.
  • "revalidate for user plans does not refetch organization plans" effectively asserts that invalidation is scoped by the for dimension, so org plans aren’t accidentally refetched when user plans are revalidated. The isRQ gating and calls.every(value => value === 'user') check express this clearly.

Together these tests give good confidence that user vs organization plan queries won’t interfere with each other while still benefiting from the generalized invalidation.

You could optionally also assert that at least one revalidation call has for: 'user' even in the RQ branch (using waitFor on both length and argument shape) to strengthen the assertion, but it’s not strictly necessary.

Also applies to: 207-278

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1)

1-211: Comprehensive revalidation coverage for useApiKeys

This new suite thoroughly exercises useApiKeys revalidation:

  • Verifies that revalidate refreshes data and increments the underlying getAll call count.
  • Confirms cascade behavior between paginated and infinite variants only in RQ mode via __CLERK_USE_RQ__.
  • Checks that cascades respect configuration boundaries (different pageSize, query, and especially different subject values).

The subject-isolation test in particular is a nice guardrail against over-broad invalidation. Overall the structure and use of act/waitFor look solid.

You might optionally tighten the “different pageSize” and “different query filters” tests by asserting that, in the RQ branch, at least one revalidation call corresponds to each configuration (e.g., checking pageSize / query values in getAllSpy.mock.calls) to more directly verify that both related queries are refetched.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4e7a7 and bd87133.

📒 Files selected for processing (7)
  • packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
  • packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2 hunks)
  • packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2 hunks)
  • packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1 hunks)
  • packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)
packages/clerk-js/src/test/utils.ts (3)
  • renderHook (77-77)
  • waitFor (73-73)
  • act (75-75)
packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx (2)
packages/shared/src/types/billing.ts (1)
  • BillingPlanResource (120-187)
packages/shared/src/react/hooks/usePlans.tsx (1)
  • usePlans (8-21)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (2)
packages/shared/src/react/hooks/useSubscription.rq.tsx (1)
  • useSubscription (43-99)
packages/shared/src/react/hooks/useSubscription.swr.tsx (1)
  • useSubscription (22-71)
packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)
packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)
  • usePagesOrInfinite (21-291)
packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx (1)
  • usePagesOrInfinite (37-237)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (6)
packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx (1)

1-1: Revalidation test for subscriptions is well-structured

Importing act and the new "revalidate fetches the latest subscription data" test correctly exercise the hook’s revalidate contract (including async behavior) and mirror patterns used elsewhere in this suite. This should protect against regressions in both RQ and SWR variants.

Also applies to: 217-233

packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts (2)

505-533: Good coverage of basic revalidate refetch semantics

The "refetches current data when revalidate is invoked" test cleanly validates that revalidate performs a fresh fetch and updates data, using act + waitFor in an async-friendly way. This aligns with the new async revalidate implementation and should remain stable across RQ/SWR implementations.


684-724: Cascade revalidation expectations are clearly specified

The "cascades revalidation to related queries only in React Query mode" test nicely captures the intended behavior: in RQ mode, invalidating via the paginated hook also refetches the infinite counterpart, while non-RQ mode keeps revalidation local. The __CLERK_USE_RQ__ gating plus lenient >= 2 assertion make this resilient to minor fetch scheduling differences.

packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx (1)

1-1: Revalidation behavior for billing hooks is well-validated

The added "revalidate behavior" suite does a good job of:

  • Verifying that revalidate actually refreshes paginated data.
  • Asserting that, in RQ mode, revalidation of the paginated hook also triggers the infinite variant while staying local in non-RQ mode.

This ties the higher-level createBillingPaginatedHook behavior back to the updated usePagesOrInfinite revalidation strategy without over-constraining call counts.

Also applies to: 490-550

packages/shared/src/react/hooks/usePageOrInfinite.types.ts (1)

21-22: Invalidation key typing matches the new revalidate implementation

Defining InvalidationQueryKey as [string, boolean, Record<string, unknown>] and wiring keys.invalidationKey to that shape lines up with how usePagesOrInfinite.rq.tsx now destructures and uses the invalidation key (stable prefix, authenticated flag, tracked keys). This tighter typing should help prevent accidentally passing full query keys or mismatched structures into the hook.

It’s worth confirming that all createCacheKeys call sites (and any custom keys objects, if they exist) still satisfy this narrower invalidationKey shape once type-checking and the test suite run.

Also applies to: 28-28

packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx (1)

268-271: Async revalidate with generalized invalidation key looks correct

The new revalidate implementation:

  • First invalidates keys.invalidationKey, which matches the paginated query key prefix.
  • Then derives [stablePrefix + '-inf', ...rest] to invalidate the corresponding infinite queries by prefix.
  • Returns the second invalidation promise, making revalidate awaitable in tests and consumers.

This matches the intended “cascade across pagination/infinite variants sharing the same invalidation key” behavior and is consistent with how tests assert fan-out in RQ mode.

Please ensure this aligns with the exact invalidateQueries prefix-matching semantics in your TanStack Query version (v5 APIs), and that no call sites relied on revalidate being strictly synchronous; the tests added in this PR already cover the new async contract, but a quick type-check / test run will confirm.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

8-14: Consider a more specific type assertion for the mock return value.

The Record<string, unknown> type for data array items is quite loose and could mask type mismatches between the mock and actual API response.

Consider defining a more specific type for the mock API key data, for example:

+type MockAPIKey = { id: string; [key: string]: unknown };+
const getAllSpy = vi.fn(
async () =>
({
data: [],
total_count: 0,
- }) as { data: Array<Record<string, unknown>>; total_count: number },+ }) as { data: Array<MockAPIKey>; total_count: number },
);

64-99: Consider extracting the RQ flag check pattern.

The pattern of checking (globalThis as any).__CLERK_USE_RQ__ and conditionally asserting call counts is repeated across multiple tests (also at lines 128, 166).

Consider extracting a helper function to reduce duplication:

constexpectCallsBasedOnRQMode=async(spy: any,rqCalls: number,nonRqCalls: number)=>{constisRQ=Boolean((globalThisasany).__CLERK_USE_RQ__);if(isRQ){awaitwaitFor(()=>expect(spy.mock.calls.length).toBeGreaterThanOrEqual(rqCalls));}else{awaitwaitFor(()=>expect(spy).toHaveBeenCalledTimes(nonRqCalls));}};

Then use it as:

awaitexpectCallsBasedOnRQMode(getAllSpy,2,1);

177-210: Simplify the subject verification assertion.

Line 209's assertion expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true) is unnecessarily complex.

Apply this diff to make the assertion clearer:

 expect(getAllSpy).toHaveBeenCalledTimes(1);
const subjects = getAllSpy.mock.calls.map(call => (call[0] as { subject?: string })?.subject);
expect(subjects).not.toContain('user_secondary');
- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(subjects[0]).toMatch(/^(user_primary)?$/);

Or even more explicitly:

- expect(subjects[0] === undefined || subjects[0] === 'user_primary').toBe(true);+ expect(['user_primary', undefined]).toContain(subjects[0]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bd87133 and a973f80.

📒 Files selected for processing (2)
  • .changeset/young-impalas-grab.md (1 hunks)
  • packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Packages
  • GitHub Check: Formatting | Dedupe | Changeset
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (4)
.changeset/young-impalas-grab.md (1)

1-14: LGTM! Clear documentation of the behavior change.

The changeset clearly describes the relaxed revalidation behavior, and the code example effectively demonstrates how revalidation now works across different hook configurations.

packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx (3)

32-38: LGTM! Proper test isolation.

The beforeEach hook correctly clears mocks and the query client, ensuring each test starts with a clean state.


40-62: LGTM! Clear revalidation test.

This test effectively verifies that calling revalidate() fetches fresh data from the API.


1-211: Excellent test coverage for revalidation behavior.

The test suite comprehensively covers:

  • Basic revalidation functionality
  • Cascading revalidation across paginated and infinite modes
  • Configuration variations (pageSize, query filters)
  • Subject isolation (negative test)
  • Behavior differences based on the RQ feature flag

All async operations are properly handled with act() and waitFor(), and test isolation is maintained through proper cleanup.

@panteliselef
panteliselef merged commit 00291bc into mainNov 17, 2025
45 checks passed
@panteliselef
panteliselef deleted the elef/improve-revalidations-in-rq-variants branch November 17, 2025 13:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@panteliselef@octoper