firebase / firebase/firebase-js-sdk

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

Open
#9,697 2 comments 2 reactions 1 assignee Claimed by @stephenarosaj View on GitHub
api: dataconnect needs-attention question stack:ReactNative
Dominant language
TypeScript
Stars
5.1k
Forks
1k
Avg merge
2d 21h
Merged PRs (30d)
37

Description

### 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:
```js
executeQuery(ref)
```

However the SDK implementation expects:
```js
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:

```js
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
```js
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

```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

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

---

### 2. Query

```graphql
query GetThings {
things {
id
text
}
}
```

---

### 3. Mutation

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

---

### 4. Fetch Data Using Generated Hook

```ts
const { data } = useGetThings();
```

---

### 5. Perform Optimistic Update

```ts
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:

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

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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.