Skip to content

feat: tanstack query plugin improvements, refetch control & state#643

Draft
DorianMazur wants to merge 6 commits intomainfrom
feat/tanstack-query-plugin-improvements
Draft

feat: tanstack query plugin improvements, refetch control & state#643
DorianMazur wants to merge 6 commits intomainfrom
feat/tanstack-query-plugin-improvements

Conversation

@DorianMazur
Copy link
Collaborator

@DorianMazur DorianMazur commented Mar 3, 2026

Problem

Two gaps in the TanStack Query integration were causing problems:

1. No distinction between re-observation and explicit sync

On main, get() called observer.refetch() on every call after the first, whether the observable was simply being re-observed (remount) or the user explicitly called state$.sync(). This meant every re-observation triggered a network request, bypassing TQ's own refetch policies (refetchOnMount, staleTime, etc.). It also meant there was no way to distinguish "the component remounted" from "the user wants fresh data now".

2. Loading & error states were silently swallowed

The subscribe callback only handled status === 'success'. Errors were ignored, initial load errors couldn't reject the get() promise, and there was no way to observe TQ's granular state (isFetching, fetchStatus, etc.).

Solution

1. Re-observation vs explicit sync

The get function now uses a subscribedInThisCycle flag to distinguish the two cases. The sync infrastructure calls subscribe() before get() on (re-)observation, but skips subscribe() on an explicit sync() when already subscribed:

Call Behavior
First get() (mount) Return cached/optimistic data from TQ; await the observer if still loading
Re-observation (subscribe() then get()) Return cached data. TQ's observer handles refetch decisions via subscription (refetchOnMount, staleTime, etc.)
Explicit sync() (get() without preceding subscribe()) Always call observer.refetch(), guaranteed fresh data

2. Error propagation & state exposure

Two layers of state propagation were added to the subscribe callback:

  • Errors flow through onError() into syncState.error, and are auto-cleared on the next success.
  • Initial load errors reject the get() promise via rejectInitialPromise.
  • onQueryStateChange a new opt-in callback for full TQ observer state:
syncedQuery({
    query: { queryKey: ['todos'] },
    queryClient,
    onQueryStateChange: ({ isLoading, isFetching, error, status, fetchStatus }) => {
        // React to any TQ state change
    },
});

interface QueryState<TError = DefaultError> {
    isLoading: boolean;
    isFetching: boolean;
    error: TError | null;
    status: 'pending' | 'error' | 'success';
    fetchStatus: 'fetching' | 'paused' | 'idle';
}

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.

1 participant