Skip to content

Data Connect React wrapper does not forward fetchPolicy to executeQuery, causing stale cache behavior with TanStack Query #9697

@KennyYe

Description

@KennyYe

Operating System

MacOS Sequia 15.7.3

Environment (if applicable)

React Native (Expo) , Firebase Data Connect, Tanstack Query v5

Firebase SDK Version

12.10.0

Firebase SDK Product(s)

Database, DataConnect

Project Tooling

React Native app using Expo

Detailed Problem Description

Detailed Problem Description

When using Firebase Data Connect with the React TanStack Query wrapper, queries that use the generated React Hooks always default to the internal Data Connect cache because the wrapper does not forward fetchPolicy to the underlying executeQuery call.

This results in stale results being returned even when TanStack Query invalidates or refetches the query.

What we were trying to achieve

Use Firebase Data Connect together with TanStack Query and perform:

  • optimistic updates
  • query invalidation
  • refetching from the server

These are standard patterns supported by TanStack Query.

What actually happens

Even when React Query refetches the query, the Firebase SDK returns cached results instead of hitting the server.

This occurs because executeQuery defaults to: fetchPolicy = PREFER_CACHE

and the React wrapper never forwards a fetchPolicy option.

Because of this, the Data Connect QueryManager cache overrides React Query's cache lifecycle.

This results in behavior like:

  1. Perform optimistic update
  2. Invalidate query
  3. React Query refetches
  4. Data Connect returns cached result instead of server result
  5. UI reverts to stale state

Root Cause

The React wrapper (useDataConnectQuery) calls executeQuery without forwarding options:

executeQuery(ref)

However the SDK implementation expects:

executeQuery(queryRef, options)

And default to: fetchPolicy = PREFER_CACHE (when options are not provided)

Because the wrapper does not expose or forward fetchPolicy, developers cannot control the caching behavior.


Relevant Code Locations

Generated React hook

packages/mobile-app/src/dataconnect-generated/react/index.cjs.js

Example generated hook:

exports.useGetThings = function useGetThings(dcOrVars, varsOrOptions, options) {
  const { dc: dcInstance, vars: inputVars, options: inputOpts } =
    validateReactArgs(connectorConfig, dcOrVars, varsOrOptions, options, true, true);

  const ref = getThingsRef(dcInstance, inputVars);

  return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}

This passes options into useDataConnectQuery.

useQueryResult from Tanstack data-connect

node_modules/@tanstack-query-firebase/react/dist/data-connect/index.js

Relevant Section

const useQueryResult = useQuery({
  ...options,
  initialData,
  queryKey: options?.queryKey ?? [ref.name, ref.variables || null],
  queryFn: async () => {
    const response = await executeQuery2(ref);
    setDataConnectResult(response);

    return {
      ...response.data
    };
  }
});

The wrapper calls: executeQuery2(ref) but does not pass any options.

This prevents the user from setting: fetchPolicy

Data Connect SDK implementation

node_modules/@firebase/data-connect/dist/index.cjs.js

function executeQuery(queryRef, options) {
    if (queryRef.refType !== QUERY_STR) {
        return Promise.reject(new DataConnectError(Code.INVALID_ARGUMENT, `ExecuteQuery can only execute query operations`));
    }

    const queryManager = queryRef.dataConnect._queryManager;

    const fetchPolicy = options?.fetchPolicy ?? QueryFetchPolicy.PREFER_CACHE;

    switch (fetchPolicy) {
        case QueryFetchPolicy.SERVER_ONLY:
            return queryManager.fetchServerResults(queryRef);
        case QueryFetchPolicy.CACHE_ONLY:
            return queryManager.fetchCacheResults(queryRef, true);
        case QueryFetchPolicy.PREFER_CACHE:
            return queryManager.preferCacheResults(queryRef, false);
        default:
            throw new DataConnectError(Code.INVALID_ARGUMENT, `Invalid fetch policy: ${fetchPolicy}`);
    }
}

Because executeQuery is called without options, the default policy is always: PREFER_CACHE

Behavior

  • Fetch query using generated hook
  • Perform optimistic update via React Query
  • Invalidate query
  • Query refetches
  • Data Connect returns cached result instead of server result

Temporary Workaround

Manually patching the wrapper to pass a fetch policy resolves the issue.

Example change:

const response = await executeQuery2(ref, {
  fetchPolicy: QueryFetchPolicy.SERVER_ONLY
});

After this change, queries correctly hit the server and React Query behaves as expected.

Expected Behavior

The React wrapper should allow users to control fetchPolicy, either by:

forwarding fetchPolicy from hook options

exposing a Data Connect options object

defaulting to SERVER_ONLY when used with TanStack Query

Actual Behavior

useDataConnectQuery prevents any control of fetchPolicy, forcing: PREFER_CACHE

which conflicts with TanStack Query's caching model.

Summary

Current architecture results in two competing cache layers:

React Query cache
+
Firebase Data Connect QueryManager cache

Because fetchPolicy cannot be configured, developers cannot disable the Data Connect cache when using TanStack Query.

Forwarding fetchPolicy from useDataConnectQuery to executeQuery would resolve this issue.

Steps and code to reproduce issue

Steps and Code to Reproduce Issue

1. Schema

type Thing @table(key: ["id"]) {
  id: UUID! @default(expr: "uuidV4()")
  text: String!
}

2. Query

query GetThings {
  things {
    id
    text
  }
}

3. Mutation

mutation UpdateThing($id: UUID!, $text: String!) {
  thing_update(
    key: { id: $id }
    data: { text: $text }
  )
}

4. Fetch Data Using Generated Hook

const { data } = useGetThings();

5. Perform Optimistic Update

const queryClient = useQueryClient();

const mutation = useUpdateThing({
  onMutate: async (variables) => {
    await queryClient.cancelQueries(["GetThings"]);

    const previous = queryClient.getQueryData(["GetThings"]);

    queryClient.setQueryData(["GetThings"], (old) => ({
      things: old.things.map((t) =>
        t.id === variables.id ? { ...t, text: variables.text } : t
      )
    }));

    return { previous };
  },

  onError: (_err, _vars, context) => {
    queryClient.setQueryData(["GetThings"], context.previous);
  },

  onSettled: () => {
    queryClient.invalidateQueries(["GetThings"]);
  }
});

6. Result

  1. GetThings is fetched using the generated Data Connect hook.
  2. A mutation performs an optimistic update.
  3. invalidateQueries(["GetThings"]) triggers a refetch.
  4. The query function executes again.
  5. Firebase Data Connect returns cached results instead of server results.

Console output shows:

source: CACHE

The UI reverts to stale data even though React Query refetched the query.


7. Workaround

If the wrapper is patched to call:

executeQuery(ref, { fetchPolicy: QueryFetchPolicy.SERVER_ONLY })

the query correctly fetches fresh server data and the issue disappears.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions