Skip to content

Conversation

@visz11
Copy link
Collaborator

@visz11 visz11 commented Oct 7, 2025

Summary by CodeRabbit

  • New Features
    • Added a new hook: useQueryState for deriving query states with optional filtering and selection.
  • Refactor
    • Streamlined internal fetching and mutation state tracking for more consistent UI updates.

@gemini-code-assist
Copy link

Summary of Changes

Hello @visz11, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new useQueryState hook, designed to provide a flexible way to access and filter query states from the query cache. It refactors the existing useIsFetching hook to utilize this new utility, streamlining its implementation. Additionally, the useMutationState hook's internal subscription mechanism has been updated to align with best practices for useSyncExternalStore, improving consistency and potentially performance.

Highlights

  • New Hook: useQueryState: Introduced a new useQueryState hook that provides a flexible way to observe and filter query states from the query cache using React.useSyncExternalStore.
  • Refactor: useIsFetching: The useIsFetching hook has been refactored to internally utilize the new useQueryState hook, simplifying its implementation and demonstrating the utility of the new generic hook.
  • Refactor: useMutationState: The internal useSyncExternalStore implementation within useMutationState has been updated to standardize its subscription and snapshot logic, improving consistency.
  • Test Improvements: Adjustments were made to test files for useIsFetching and useMutationState, including wrapping components with React.memo to prevent unnecessary re-renders in tests.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@refacto-test
Copy link

refacto-test bot commented Oct 7, 2025

Clone use query state

TL;DR: Refactors React Query hooks to use a new shared useQueryState hook for consistent state management and improved performance.


Refacto PR Summary

Introduces useQueryState as a centralized hook for query state management, replacing direct query cache subscriptions.
Refactors useIsFetching to leverage the new shared hook and optimizes useMutationState subscription handling. This PR consolidates query state management logic by creating a reusable useQueryState hook that handles query cache subscriptions and state updates. The implementation uses useSyncExternalStore with proper batching and deep equality checks to prevent unnecessary re-renders. The useIsFetching hook is simplified to use this new abstraction, while useMutationState receives performance improvements through better subscription handling and state comparison logic.

Change Highlights

Click to expand
  • src/useQueryState.ts: New shared hook for query state management with filtering and selection
  • src/useIsFetching.ts: Refactored to use useQueryState instead of direct cache subscriptions
  • src/useMutationState.ts: Optimized subscription handling with batched notifications
  • src/index.ts: Exports new useQueryState hook for public API
  • tests/useIsFetching.test.tsx: Moved hook call before useQuery to fix timing issues
  • tests/useMutationState.test.tsx: Added React.memo to prevent unwanted re-renders in tests

Sequence Diagram

sequenceDiagram
    participant C as Component
    participant UQS as useQueryState
    participant QC as QueryCache
    participant SE as SyncExternalStore
    
    C->>UQS: useQueryState(filters)
    UQS->>QC: getQueryCache()
    UQS->>SE: useSyncExternalStore(subscribe, getSnapshot)
    QC->>SE: subscribe to cache changes
    QC->>QC: Query state changes
    QC-->>SE: Notify subscribers (batched)
    SE->>UQS: Trigger getSnapshot()
    UQS->>UQS: replaceEqualDeep comparison
    UQS-->>C: Return filtered query states
Loading

Testing Guide

Click to expand
  1. Query state filtering: Create queries with different statuses, verify useQueryState returns correct filtered results
  2. Fetching count accuracy: Trigger multiple queries, confirm useIsFetching returns accurate count using new implementation
  3. Performance optimization: Monitor re-render frequency with mutation state changes, verify batched notifications reduce updates
  4. Hook timing: Test useIsFetching called before useQuery works correctly without race conditions
  5. Deep equality: Modify query state with same values, confirm components don't re-render unnecessarily

@coderabbitai
Copy link

coderabbitai bot commented Oct 7, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Introduces a new hook useQueryState and exports it. Refactors useIsFetching to derive its value via useQueryState. Adjusts useMutationState internals to batch notifications and compute results lazily. Updates related tests: minor hook placement change in useIsFetching test and memoized components in useMutationState tests.

Changes

Cohort / File(s) Summary
New hook and export
packages/react-query/src/useQueryState.ts, packages/react-query/src/index.ts
Adds useQueryState hook to derive query state arrays with optional filters/selectors; re-exports it from index.ts.
Refactor useIsFetching
packages/react-query/src/useIsFetching.ts
Replaces manual subscription logic with useQueryState-based derivation; returns length of filtered results. Removes direct notifyManager/Provider usage.
Mutation state internals
packages/react-query/src/useMutationState.ts
Switches to mutationCache.subscribe(notifyManager.batchCalls(...)); computes next result in getter, updates cached result only on change.
Tests: useIsFetching
packages/react-query/src/__tests__/useIsFetching.test.tsx
Moves useIsFetching invocation into Page component scope; removes duplicate call.
Tests: useMutationState
packages/react-query/src/__tests__/useMutationState.test.tsx
Renames internal component to IsMutatingBase and wraps with React.memo as IsMutating in affected tests to avoid unrelated re-renders.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Component (Page)
  participant UIF as useIsFetching
  participant UQS as useQueryState
  participant QC as QueryClient
  participant QCache as QueryCache

  rect rgba(230,245,255,0.5)
    Note over C: Render phase
    C->>UIF: call(filters, queryClient?)
    UIF->>UQS: derive({ ...filters, fetchStatus: 'fetching' }, queryClient?)
  end

  rect rgba(240,255,240,0.5)
    UQS->>QC: useQueryClient() / resolve client
    UQS->>QCache: get queries matching filters
    UQS-->>UIF: return Array<TResult>
    UIF-->>C: return count (array.length)
  end

  rect rgba(255,248,230,0.5)
    Note over UQS,QCache: Subscription (useSyncExternalStore)
    UQS->>QCache: subscribe(batched)
    QCache-->>UQS: notify on changes
    UQS-->>UIF: recompute and memoize results
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

In burrows of state, I thump with delight,
A new hook sprouts—query fields in sight! 🌱
Fetching counts hop in tidy array,
Mutations hush, batched all the way.
I twitch my whiskers, tests snug and bright—
Cache winds whisper, “All’s set right.” 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title references “use query state,” which corresponds to the new hook introduced, but the phrasing “Clone use query state” is ambiguous and does not clearly describe that a new useQueryState hook is being added or that related hooks and tests are being refactored. As a result, it only partially relates to the main changes and remains unclear to readers scanning the PR history. Please update the title to clearly state the primary change, for example “Add useQueryState hook and refactor useIsFetching to use it,” so that the main intent of the pull request is immediately understandable.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 clone-use-query-state

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05e18d3 and efa8623.

📒 Files selected for processing (1)
  • packages/react-query/src/useQueryState.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react-query/src/useQueryState.ts
⏰ 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). (1)
  • GitHub Check: Test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new useQueryState hook and refactors useIsFetching to leverage it, which is a great abstraction. It also updates useMutationState to follow a similar implementation pattern for consistency. However, I've identified a potential issue in both useQueryState and the updated useMutationState regarding how options are handled within useSyncExternalStore. The current pattern of using a useRef updated via useEffect can lead to the component rendering with stale data for one cycle when options change. My review includes specific suggestions to resolve this by using the options prop directly in the getSnapshot function.

() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),

Choose a reason for hiding this comment

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

high

Using optionsRef.current here can lead to stale data. The optionsRef is updated in a useEffect, which runs after the render pass where getSnapshot is executed. This means getSnapshot might be using stale options.

You should use the options prop directly from the hook's arguments, as getSnapshot is recreated on each render and will have access to the latest options via its closure. This would also allow removing optionsRef and the corresponding useEffect from the hook.

Suggested change
getResult(mutationCache, optionsRef.current),
getResult(mutationCache, options),

Comment on lines 37 to 66
const queryCache = useQueryClient(queryClient).getQueryCache()
const optionsRef = React.useRef(options)
const result = React.useRef<Array<TResult>>()
if (!result.current) {
result.current = getResult(queryCache, options)
}

React.useEffect(() => {
optionsRef.current = options
})

return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
[queryCache],
),
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(queryCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
}

return result.current
},
() => result.current,
)!

Choose a reason for hiding this comment

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

high

The current implementation using optionsRef and useEffect can lead to returning stale data for one render cycle when the options prop changes. This is because useEffect runs after rendering, so optionsRef.current is updated too late for useSyncExternalStore's getSnapshot function, which runs during rendering.

A better approach is to remove optionsRef and useEffect, and use the options prop directly inside getSnapshot. Since getSnapshot is redefined on each render, it will correctly close over the latest options.

  const queryCache = useQueryClient(queryClient).getQueryCache()
  const result = React.useRef<Array<TResult>>()

  // Eagerly calculate result for first render and server render
  if (result.current === undefined) {
    result.current = getResult(queryCache, options)
  }

  return React.useSyncExternalStore(
    React.useCallback(
      (onStoreChange) =>
        queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
      [queryCache],
    ),
    () => {
      const nextResult = replaceEqualDeep(
        result.current,
        getResult(queryCache, options),
      )
      if (result.current !== nextResult) {
        result.current = nextResult
      }
      return result.current
    },
    () => result.current,
  )!

Copy link

@coderabbitai coderabbitai bot left a comment

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 (1)
packages/react-query/src/useQueryState.ts (1)

21-67: Consider extracting shared logic to reduce duplication.

The implementation is correct and follows best practices for useSyncExternalStore. However, this hook shares nearly identical structure with useMutationState (lines 33-47 and 49-83 in useMutationState.ts):

  • Both use useSyncExternalStore with batched notifications
  • Both use replaceEqualDeep for efficient updates
  • Both maintain stable refs with identical update patterns

Consider extracting a generic helper hook like useCacheState to reduce duplication and simplify maintenance. This would centralize the subscription logic and ensure consistent behavior across both hooks.

Example structure for a shared helper:

function useCacheState<TEntity, TResult>(
  cache: { subscribe: (callback: () => void) => () => void, findAll: (filters: any) => TEntity[] },
  options: { filters?: any, select?: (entity: TEntity) => TResult },
  defaultResult: (entity: TEntity) => any
): Array<TResult> {
  // Shared useSyncExternalStore logic here
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a5990e and 05e18d3.

📒 Files selected for processing (6)
  • packages/react-query/src/__tests__/useIsFetching.test.tsx (1 hunks)
  • packages/react-query/src/__tests__/useMutationState.test.tsx (3 hunks)
  • packages/react-query/src/index.ts (1 hunks)
  • packages/react-query/src/useIsFetching.ts (1 hunks)
  • packages/react-query/src/useMutationState.ts (1 hunks)
  • packages/react-query/src/useQueryState.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/react-query/src/__tests__/useMutationState.test.tsx (1)
packages/react-query/src/useMutationState.ts (1)
  • useIsMutating (15-24)
packages/react-query/src/useQueryState.ts (1)
packages/query-core/src/types.ts (2)
  • DefaultError (18-22)
  • QueryKey (24-24)
packages/react-query/src/useIsFetching.ts (2)
packages/query-core/src/queryClient.ts (1)
  • QueryClient (54-550)
packages/react-query/src/useQueryState.ts (1)
  • useQueryState (33-67)
packages/react-query/src/__tests__/useIsFetching.test.tsx (2)
packages/query-core/src/queryClient.ts (1)
  • isFetching (102-105)
packages/react-query/src/useIsFetching.ts (1)
  • useIsFetching (5-13)
⏰ 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). (1)
  • GitHub Check: Preview
🔇 Additional comments (5)
packages/react-query/src/useMutationState.ts (1)

67-82: LGTM! Batching and deep equality optimization.

The changes improve performance by batching cache notifications and using deep equality checks to prevent unnecessary re-renders. The pattern correctly leverages useSyncExternalStore with stable result references.

packages/react-query/src/index.ts (1)

43-43: LGTM! Public API extension.

The export makes useQueryState available as part of the public API, consistent with other state hooks like useMutationState.

packages/react-query/src/__tests__/useIsFetching.test.tsx (1)

208-208: LGTM! Correct hook placement.

Moving the useIsFetching call inside the Page component scope is correct and aligns with React hook rules. The explicit queryClient parameter ensures the test uses the intended client instance.

packages/react-query/src/__tests__/useMutationState.test.tsx (1)

69-76: LGTM! Test isolation with memoization.

The React.memo wrappers correctly isolate the IsMutating component from parent re-renders triggered by useMutation hooks. This ensures the test accurately measures useIsMutating's subscription behavior without noise from unrelated state changes.

This pattern improves test reliability by focusing on the specific hook's behavior.

Also applies to: 110-119

packages/react-query/src/useIsFetching.ts (1)

9-12: LGTM! Clean refactor leveraging useQueryState.

The refactor simplifies useIsFetching by delegating to the new useQueryState hook with fetchStatus: 'fetching' filter. This matches the previous behavior (as confirmed by the queryClient.isFetching implementation in the relevant snippets) while reducing code duplication and maintenance burden.

@visz11
Copy link
Collaborator Author

visz11 commented Oct 7, 2025

/refacto-visz

@refacto-visz
Copy link

refacto-visz bot commented Oct 7, 2025

Refacto is reviewing this PR. Please wait for the review comments to be posted.

@refacto-visz
Copy link

refacto-visz bot commented Oct 7, 2025

Code Review: React Query State Management

👍 Well Done
Consistent Pattern Implementation

New useQueryState follows established useMutationState pattern maintaining architectural consistency.

Code Reuse Implementation

Excellent refactoring of useIsFetching to leverage shared useQueryState hook.

📁 Selected files for review (6)
  • packages/react-query/src/useQueryState.ts
  • packages/react-query/src/useMutationState.ts
  • packages/react-query/src/useIsFetching.ts
  • packages/react-query/src/__tests__/useMutationState.test.tsx
  • packages/react-query/src/__tests__/useIsFetching.test.tsx
  • packages/react-query/src/index.ts
🎯 Custom Instructions
✅ Applied Instructions
Organization Guidelines
  • Reuse code wherever possible and avoid redundant code by refactoring with utils and static method across application

Scope: All files

❌ Unapplied Instructions
portal-backend

Reason: Repository 'portal-backend' does not match current PR repository

refacto-api

Reason: Repository 'refacto-api' does not match current PR repository

pr-reviewer

Reason: Repository 'pr-reviewer' does not match current PR repository

mypy

Reason: Repository 'mypy' does not match current PR repository

bazel

Reason: Repository 'bazel' does not match current PR repository

devd-client

Reason: Repository 'devd-client' does not match current PR repository

📝 Additional Comments
packages/react-query/src/useQueryState.ts (4)
Code Duplication Violation

New useQueryState implementation duplicates substantial logic from useMutationState pattern without reusing existing code. Organization guideline requires code reuse and redundancy avoidance through utils and static methods. Implementation creates maintenance burden with identical useSyncExternalStore pattern and result caching logic.

Standards:

  • Org-Guideline-Reuse code wherever possible and avoid redundant code by refactoring with utils and static method across application
  • Code-Reusability-Pattern-Consistency
  • Maintenance-Efficiency-DRY
Missing Generic Abstraction

Similar cache state management pattern exists in both query and mutation hooks but lacks shared abstraction. Consider creating generic useCacheState hook to eliminate pattern duplication and improve maintainability across cache-based state hooks.

Standards:

  • Org-Guideline-Reuse code wherever possible and avoid redundant code by refactoring with utils and static method across application
  • SOLID-DRY
  • Design-Pattern-Template-Method
Subscription Cleanup Missing

Query cache subscription lacks explicit cleanup mechanism validation. While useSyncExternalStore handles unsubscription internally, subscription pattern should return unsubscribe function for reliability. Missing explicit cleanup could cause memory leaks if subscription management fails.

Standards:

  • ISO-IEC-25010-Reliability-Recoverability
  • SRE-Resource-Management
Efficient State Management

Excellent use of deep equality comparison prevents unnecessary re-renders by only updating state when actual changes occur. This optimization reduces React reconciliation overhead and improves component performance.

Standards:

  • ISO-IEC-25010-Performance-Efficiency-Time-Behavior
  • Optimization-Pattern-Memoization
packages/react-query/src/useIsFetching.ts (1)
Filter Optimization Opportunity

Object spread operation creates new filter object on every render potentially causing unnecessary re-computations in useQueryState. Consider memoizing the filter object to prevent redundant query cache filtering operations when filters haven't changed.

Standards:

  • ISO-IEC-25010-Performance-Efficiency-Time-Behavior
  • Optimization-Pattern-Memoization
packages/react-query/src/useMutationState.ts (1)
Pattern Consolidation Opportunity

Updated useMutationState pattern now matches useQueryState implementation creating opportunity for shared utility function. Both hooks use identical result caching and equality checking logic. Consolidating this pattern would align with organization guideline for code reuse and reduce maintenance overhead.

Standards:

  • Org-Guideline-Reuse code wherever possible and avoid redundant code by refactoring with utils and static method across application
  • Pattern-Extraction-Opportunity
  • Code-Consolidation-Benefits

Comment on lines +54 to +64
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(queryCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
}

return result.current
},
Copy link

Choose a reason for hiding this comment

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

Duplicated State Logic

Identical state management logic exists in both useQueryState and useMutationState hooks. This violates DRY principle creating maintenance overhead when state handling needs modification. Extract shared state management utility to eliminate code duplication.

    () => {
      return getStateResult(result, () => getResult(queryCache, optionsRef.current))
    },
Commitable Suggestion
Suggested change
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(queryCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
}
return result.current
},
() => {
return getStateResult(result, () => getResult(queryCache, optionsRef.current))
},
Standards
  • Org-Guideline-Reuse code wherever possible and avoid redundant code by refactoring with utils and static method across application
  • Clean-Code-DRY
  • Refactoring-Extract-Function

Comment on lines +48 to +53
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
[queryCache],
),
Copy link

Choose a reason for hiding this comment

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

Duplicated Subscription Pattern

Subscription pattern with notifyManager.batchCalls is duplicated across useQueryState and useMutationState. This creates redundant implementation that must be maintained in multiple locations. Refactor into shared subscription utility function.

  return React.useSyncExternalStore(
    React.useCallback(
      (onStoreChange) => createCacheSubscription(queryCache, onStoreChange),
      [queryCache],
    ),
Commitable Suggestion
Suggested change
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
[queryCache],
),
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) => createCacheSubscription(queryCache, onStoreChange),
[queryCache],
),
Standards
  • Org-Guideline-Reuse code wherever possible and avoid redundant code by refactoring with utils and static method across application
  • Clean-Code-DRY
  • Design-Pattern-Template-Method

Comment on lines 64 to 82
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
mutationCache.subscribe(() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
notifyManager.schedule(onStoreChange)
}
}),
mutationCache.subscribe(notifyManager.batchCalls(onStoreChange)),
[mutationCache],
),
() => result.current,
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
}

return result.current
},
() => result.current,
)!
Copy link

Choose a reason for hiding this comment

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

Code Duplication Violation

Violates organization guideline requiring code reuse and redundancy elimination. The useSyncExternalStore pattern with cache subscription and result comparison is duplicated between useMutationState and useQueryState. This creates maintenance overhead and potential inconsistencies in state synchronization logic.

    return React.useSyncExternalStore(
      React.useCallback(
        (onStoreChange) => createCacheSubscription(mutationCache, onStoreChange),
        [mutationCache],
      ),
      () => getStateResult(result, () => getResult(mutationCache, optionsRef.current)),
      () => result.current,
    )!
Commitable Suggestion
Suggested change
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
mutationCache.subscribe(() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
notifyManager.schedule(onStoreChange)
}
}),
mutationCache.subscribe(notifyManager.batchCalls(onStoreChange)),
[mutationCache],
),
() => result.current,
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
}
return result.current
},
() => result.current,
)!
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) => createCacheSubscription(mutationCache, onStoreChange),
[mutationCache],
),
() => getStateResult(result, () => getResult(mutationCache, optionsRef.current)),
() => result.current,
)!
Standards
  • Org-Guideline-Reuse code wherever possible and avoid redundant code by refactoring with utils and static method across application
  • ISO-IEC-25010-Performance-Efficiency-Resource-Utilization

Comment on lines 44 to 46
React.useEffect(() => {
optionsRef.current = options
})
Copy link

Choose a reason for hiding this comment

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

State Synchronization Race

Options reference update lacks dependency array causing potential stale closure issues. Effect runs after every render potentially creating race conditions between option updates and state synchronization. Missing dependency array prevents React optimization and creates reliability gaps.

Standards
  • ISO-IEC-25010-Reliability-Fault-Tolerance
  • ISO-IEC-25010-Functional-Correctness-Appropriateness

Co-authored-by: refacto-visz[bot] <230139763+refacto-visz[bot]@users.noreply.github.com>
@visz11
Copy link
Collaborator Author

visz11 commented Oct 7, 2025

/refacto-visz

@refacto-visz
Copy link

refacto-visz bot commented Oct 7, 2025

Refacto is reviewing this PR. Please wait for the review comments to be posted.

@refacto-visz
Copy link

refacto-visz bot commented Oct 7, 2025

Code Review: React Query State Management

👍 Well Done
Consistent State Pattern

Unified state management approach across query and mutation hooks using useSyncExternalStore.

📁 Selected files for review (6)
  • packages/react-query/src/useQueryState.ts
  • packages/react-query/src/useMutationState.ts
  • packages/react-query/src/useIsFetching.ts
  • packages/react-query/src/__tests__/useMutationState.test.tsx
  • packages/react-query/src/__tests__/useIsFetching.test.tsx
  • packages/react-query/src/index.ts
🎯 Custom Instructions
✅ Applied Instructions
Organization Guidelines
  • Reuse code wherever possible and avoid redundant code by refactoring with utils and static method across application

Scope: All files

❌ Unapplied Instructions
portal-backend

Reason: Repository 'portal-backend' does not match current PR repository

refacto-api

Reason: Repository 'refacto-api' does not match current PR repository

pr-reviewer

Reason: Repository 'pr-reviewer' does not match current PR repository

mypy

Reason: Repository 'mypy' does not match current PR repository

bazel

Reason: Repository 'bazel' does not match current PR repository

devd-client

Reason: Repository 'devd-client' does not match current PR repository

📝 Additional Comments
packages/react-query/src/useQueryState.ts (3)
Stale Options Reference

Using optionsRef.current in getSnapshot creates stale data race condition. The useEffect updating optionsRef runs after render, but getSnapshot executes during render, causing one render cycle with outdated options. This breaks React's synchronization model and can return incorrect query results.

Standards:

  • Algorithm-Correctness-State-Synchronization
  • Business-Rule-React-Hooks-Timing
Stale Options Reference

Using optionsRef.current in getSnapshot creates stale data race condition. useEffect updates optionsRef after render, but getSnapshot executes during render, causing one render cycle with outdated options. This leads to incorrect query filtering and potential UI inconsistencies.

Standards:

  • ISO-IEC-25010-Reliability-Fault-Tolerance
  • React-Hooks-Correctness
  • Algorithm-Opt-Closure-Optimization
Stale Options Reference

Using optionsRef.current in getSnapshot creates stale data issues since useEffect runs after render but getSnapshot executes during render. This timing mismatch causes one render cycle with outdated options, blocking reliable state synchronization for team development.

Standards:

  • Clean-Code-Functions
  • React-Hooks-Correctness
  • Refactoring-Remove-Assignments
packages/react-query/src/useMutationState.ts (1)
Stale Mutation Options

Identical stale options reference issue as useQueryState. optionsRef.current accessed during render before useEffect updates it, causing mutation filtering with outdated options. Results in incorrect mutation state tracking and potential component state inconsistencies.

Standards:

  • ISO-IEC-25010-Reliability-Fault-Tolerance
  • React-Hooks-Correctness
  • Algorithm-Opt-Closure-Optimization

Comment on lines +44 to +46
React.useEffect(() => {
optionsRef.current = options
}, [options])
Copy link

Choose a reason for hiding this comment

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

Stale Options Reference

Options reference updated after render cycle creates timing window where getSnapshot uses stale options. This race condition can cause inconsistent query state during rapid option changes, leading to UI displaying outdated data.

  // Update ref synchronously to avoid stale options in getSnapshot
  optionsRef.current = options

  React.useEffect(() => {
    // Effect kept for dependency tracking
  }, [options])
Commitable Suggestion
Suggested change
React.useEffect(() => {
optionsRef.current = options
}, [options])
// Update ref synchronously to avoid stale options in getSnapshot
optionsRef.current = options
React.useEffect(() => {
// Effect kept for dependency tracking
}, [options])
Standards
  • ISO-IEC-25010-Reliability
  • SRE-State-Consistency
  • DbC-Temporal-Safety

Comment on lines +70 to +73
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
Copy link

Choose a reason for hiding this comment

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

Mutation State Staleness

Similar stale options reference pattern in mutation state hook creates data consistency issues. The optionsRef update lag during render cycles can cause mutation state to reflect outdated filter criteria.

() => {
  // Ensure we're using latest options by updating ref synchronously
  optionsRef.current = options
  
  const nextResult = replaceEqualDeep(
    result.current,
    getResult(mutationCache, optionsRef.current),
Commitable Suggestion
Suggested change
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
() => {
// Ensure we're using latest options by updating ref synchronously
optionsRef.current = options
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
Standards
  • ISO-IEC-25010-Reliability
  • SRE-State-Consistency
  • DbC-Temporal-Safety

Comment on lines +44 to +46
React.useEffect(() => {
optionsRef.current = options
}, [options])
Copy link

Choose a reason for hiding this comment

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

Stale Data Race

Race condition between useEffect updating optionsRef and useSyncExternalStore's getSnapshot function. During render cycles, getSnapshot may access stale options before useEffect runs, potentially exposing incorrect query state data to components.

Standards
  • CWE-362
  • CWE-367

Comment on lines +70 to +73
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
Copy link

Choose a reason for hiding this comment

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

Mutation State Race

Same race condition pattern as useQueryState where optionsRef updates lag behind render cycles. Components may receive stale mutation state data during option changes, potentially affecting UI security decisions based on mutation status.

Standards
  • CWE-362
  • CWE-367

Comment on lines +44 to +46
React.useEffect(() => {
optionsRef.current = options
}, [options])
Copy link

Choose a reason for hiding this comment

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

Missing Effect Dependencies

useEffect lacks dependency array causing it to run on every render instead of only when options change. This violates React hooks correctness rules and creates unnecessary ref updates, potentially causing performance issues and unpredictable synchronization behavior.

Standards
  • Algorithm-Correctness-Effect-Dependencies
  • Business-Rule-React-Hooks-Correctness

Comment on lines +48 to +51
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
Copy link

Choose a reason for hiding this comment

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

Hook Logic Duplication

Identical useSyncExternalStore pattern duplicated between useQueryState and useMutationState creates maintenance overhead when state synchronization logic needs updates. Extract shared hook utility to eliminate code duplication and ensure consistent behavior across query management hooks.

Standards
  • Clean-Code-DRY
  • Refactoring-Extract-Function
  • SOLID-SRP

Sign up for free to 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.

4 participants