React Query Offline-First 전략
staleTime, persistence, and sync patterns to implement cache-first UX in unstable network environments

Introduction
In an environment where the mobile network is unstable, data retrieval failure immediately leads to withdrawal. If you use React Query with only the default settings, the loading spinner will be exposed repeatedly, and the user will experience the status bouncing on the same screen. This article summarizes practical patterns that improve the quality of service experience by combining cache-first rendering, offline queuing, and online recovery point synchronization.

Problem definition
Most offline-first strategies fail because they are missing product policies, not library settings. The following three common failure conditions are:
- The same Query Key does not include user context, so caches are mixed when switching accounts.
- The mutation retry policy is separated from the screen flow, so it looks like success to the user, but the reflection on the server fails.
- Optimization by traffic pattern is impossible because staleTime and gcTime are applied simultaneously.
The key is to not treat network state the same as UX state. Policies for each function must be separated by allowing reading even when offline and queuing only writes.
Key concepts
| perspective | Design criteria | Verification points |
|---|---|---|
| Inquiry Policy | Cache priority + background revalidation | Whether to block API calls when rendering the first screen |
| Writing Policy | offline queue + server idempotency | Whether or not duplicate retransmissions are reflected |
| Recovery Policy | Sequential flush during online events | Data crash rate immediately after recovery |
| observation indicators | cache hit rate + retry success | Number of perceived failures per session |
Offline strategy is not just a front-end issue. If the backend does not support idempotency-key, the queuing strategy will create data corruption in the long run. Therefore, it must be designed together with the API contract.
Code example 1: Separate QueryClient default policy
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
gcTime: 10 * 60_000,
retry: 1,
refetchOnWindowFocus: false,
networkMode: "offlineFirst",
},
mutations: {
retry: 3,
networkMode: "offlineFirst",
},
},
});
export const key = {
feed: (userId: string) => ["feed", userId],
};
Code example 2: Offline queue flush
type PendingWrite = { id: string; endpoint: string; body: unknown; key: string };
const queue: PendingWrite[] = [];
export async function flushPendingWrites() {
for (const item of queue) {
const response = await fetch(item.endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": item.key,
},
body: JSON.stringify(item.body),
});
if (!response.ok) break;
}
}
window.addEventListener("online", () => {
void flushPendingWrites();
});
Architecture flow
Tradeoffs
- If you set the staleTime longer, API calls will decrease, but the responsibility for data freshness management will shift to the UI.
- Adding an offline queue improves UX, but the server idempotency must be adjusted.
- Increasing the number of retries increases the success rate, but if there is no diagnosis log for the cause of failure, response to failures becomes slower.
Cleanup
The core of React Query Offline-First is not option optimization but operational contracting. By separating lookup/write/recovery and specifying retry rules along with the API, you can maintain a stable user experience even in environments with low network quality.
Image source
- Cover: source link
- License: CC BY-SA 3.0 / Author: Dave Gandy
- Note: After downloading the free license image from Wikimedia Commons, it was optimized to JPG at 1600px.