# React Query: The Misunderstood Data-Fetching Library

Despite its name and reputation as a "data-fetching library," React Query doesn't actually fetch anything. It's a common misconception that needs clearing up.

## What React Query Really Is

React Query is **data-fetching agnostic**. It's purely a caching and state management library that doesn't care *how* you fetch your data. Whether you use `fetch()`, `axios`, `XMLHttpRequest`, or even a custom GraphQL client, React Query treats them all the same.

Think of React Query as a sophisticated data orchestrator. You're responsible for choosing a data fetcher, then React Query handles storing it, synchronizing it across components, and only refetching after the specified stale time.

## Understanding the Architecture

React Query's architecture separates concerns beautifully:

**Your Responsibility (The Data Fetcher):**

* Choose your HTTP client (fetch, axios, etc.)
    
* Define API endpoints and request structure
    
* Handle request formatting (REST, GraphQL, etc.)
    
* Configure authentication and headers
    

**React Query's Responsibility (The Cache Manager):**

* Store fetched data in memory
    
* Track cache freshness and staleness
    
* Coordinate when to refetch
    
* Manage loading and error states across your app
    

This separation means you have complete flexibility in how you retrieve data, while React Query handles the complex orchestration of keeping that data fresh and accessible.

## You Can Use ANY Fetching Method

Here's the beauty of React Query's agnostic approach—all of these work equally well:

```javascript
// Option 1: Native fetch()
const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json())
});

// Option 2: Axios
const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => axios.get(`/api/users/${userId}`).then(res => res.data)
});

// Option 3: GraphQL client
const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => graphqlClient.request(GET_USER_QUERY, { userId })
});

// Option 4: Your custom API wrapper
const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => myCustomApi.users.getById(userId)
});
```

Notice the pattern? React Query only cares that your `queryFn` returns a Promise. Everything else is up to you.

## What React Query DOES Handle

React Query excels at server state management and data synchronization:

✅ **Caches results** - Stores data in memory with configurable cache times, eliminating redundant requests

✅ **Manages loading/error states** - Provides `isLoading`, `isError`, `error`, `isFetching` without manual useState calls

✅ **Handles refetching/invalidation** - Automatically refetches stale data and invalidates caches when mutations occur

✅ **Prevents duplicate requests** - Deduplicates simultaneous requests for the same data across your entire app

✅ **Background updates** - Refetches data in the background when windows refocus, or network reconnects

✅ **Pagination/infinite scroll helpers** - Built-in utilities for complex data loading patterns

✅ **Optimistic updates** - Update UI immediately before the server confirms changes

✅ **Global state synchronization** - Automatically updates all components using the same query when data changes

✅ **Request retry logic** - Configurable retry attempts for failed requests

✅ **Garbage collection** - Removes unused cache entries to prevent memory leaks

## What React Query DOESN'T Do

Understanding the boundaries is just as important:

❌ **Make HTTP requests** - You provide the fetching function

❌ **Know about REST/GraphQL** - It's protocol-agnostic

❌ **Handle network layer** - No built-in retry logic, interceptors, or request transformation (though you can add these yourself)

## Our Real-World Implementation

In our production codebase, we chose the native `fetch()` API to make GraphQL requests. React Query manages the entire data lifecycle:

```javascript
const useProject = (projectId) => {
  return useQuery({
    queryKey: ['project', projectId],
    queryFn: async () => {
      const response = await fetch('/graphql', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          query: `
            query GetProject($id: ID!) {
              project(id: $id) {
                id
                name
                description
                updatedAt
              }
            }
          `,
          variables: { id: projectId }
        })
      });
      
      const { data, errors } = await response.json();
      if (errors) throw new Error(errors[0].message);
      return data.project;
    },
    staleTime: 5 * 60 * 000, // React Query manages staleness
    cacheTime: 10 * 60 * 1000, // React Query manages cache
  });
};
```

We handle the GraphQL request formatting and network call. React Query handles caching that response, tracking when it becomes stale, managing loading states across components, and automatically refetching when needed.

## Understanding staleTime vs cacheTime (gcTime)

Two of React Query's most important but confusing configurations are `staleTime` and `cacheTime` (renamed to `gcTime` in v5). They control different aspects of the data lifecycle:

**staleTime - When data becomes "stale"**

This determines how long data is considered "fresh." Fresh data won't trigger a refetch.

```javascript
useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser,
  staleTime: 5 * 60 * 1000, // 5 minutes
});
```

* Default: `0` (immediately stale)
    
* When staleTime expires, data becomes "stale" but remains in cache
    
* Stale data triggers background refetches on window focus, component mount, or manual refetch
    
* Fresh data (within staleTime) won't refetch even if you revisit the component
    

**cacheTime/gcTime - When cache gets garbage collected**

This determines how long unused data stays in memory after all components are unmounted.

```javascript
useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser,
  staleTime: 5 * 60 * 1000,     // Fresh for 5 minutes
  cacheTime: 10 * 60 * 1000,    // Cached for 10 minutes (v4)
  // gcTime: 10 * 60 * 1000,    // In v5, use gcTime instead
});
```

* Default: `5 minutes`
    
* Starts counting down when the last component using this query unmounts
    
* When gcTime expires, data is removed from memory completely
    
* Next fetch will be a fresh request, not served from cache
    

**How They Work Together:**

```javascript
Time:     0s -------- 5min -------- 10min -------- 15min
          |           |             |              |
Mount --> [--FRESH---][---STALE----]              |
          |           |             |              |
Unmount -------->     |             |              |
                      |  [--CACHED--][--DELETED--]
```

**Real-world example:**

```javascript
// User profile - changes infrequently
useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  staleTime: 10 * 60 * 1000,  // Don't refetch for 10 minutes
  gcTime: 30 * 60 * 1000,     // Keep in cache for 30 minutes
});

// Real-time stock prices - always fresh
useQuery({
  queryKey: ['stock', symbol],
  queryFn: fetchStock,
  staleTime: 0,               // Always stale, always refetch
  gcTime: 1 * 60 * 1000,      // Clear from cache after 1 minute
});

// Static reference data - rarely changes
useQuery({
  queryKey: ['countries'],
  queryFn: fetch
```

## The Bottom Line

React Query isn't a data-fetching library—it's a **server state management and synchronization library**. This distinction matters because it means you maintain full control over your data layer while delegating the complex state orchestration to a battle-tested solution.

## Why "State Management" Matters

Traditional state management libraries like Redux or Zustand are designed for **client state**—data that lives entirely in your application (UI state, form inputs, user preferences). Server state is fundamentally different:

**Server State Characteristics:**

* Asynchronously fetched from remote sources
    
* Can become outdated or "stale"
    
* Shared across multiple components and users
    
* Requires loading and error states
    
* Needs periodic refreshing
    
* Benefits of caching to reduce network requests
    

React Query specializes in server state. It solves problems that client state managers weren't designed for:

```javascript
// Without React Query - manual state management
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [lastFetch, setLastFetch] = useState(null);

useEffect(() => {
  const fetchData = async () => {
    setIsLoading(true);
    try {
      const result = await fetch('/api/data');
      setData(await result.json());
      setLastFetch(Date.now());
    } catch (err) {
      setError(err);
    } finally {
      setIsLoading(false);
    }
  };
  
  // Need to track staleness manually
  const isStale = !lastFetch || Date.now() - lastFetch > 60000;
  if (isStale) fetchData();
}, [lastFetch]);

// With React Query - automatic server state management
const { data, isLoading, error } = useQuery({
  queryKey: ['data'],
  queryFn: () => fetch('/api/data').then(res => res.json()),
  staleTime: 60000
});
```

React Query automatically handles what you'd need dozens of lines to manage manually: caching, staleness tracking, background refetching, request deduplication, and state synchronization across components.

## Using React Query as Global State Management

One of React Query's most powerful features is automatic global state synchronization. When you fetch data with the same `queryKey` in multiple components, they all share the same cached data and update together:

```javascript
// UserProfile.jsx
function UserProfile() {
  const { data: user } = useQuery({
    queryKey: ['user', 'current'],
    queryFn: fetchCurrentUser,
  });
  
  return <div>{user.name}</div>;
}

// UserAvatar.jsx (different component, same data!)
function UserAvatar() {
  const { data: user } = useQuery({
    queryKey: ['user', 'current'],
    queryFn: fetchCurrentUser,
  });
  
  return <img src={user.avatar} />;
}

// Settings.jsx (updates everywhere automatically)
function Settings() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: updateUserName,
    onSuccess: (updatedUser) => {
      // This updates BOTH UserProfile and UserAvatar instantly
      queryClient.setQueryData(['user', 'current'], updatedUser);
    }
  });
  
  return <button onClick={() => mutation.mutate({ name: 'New Name' })}>
    Update Name
  </button>;
}
```

**What just happened?**

1. Multiple components use the same `queryKey: ['user', 'current']`
    
2. React Query fetches **once** and shares the result
    
3. When we update the cache with `setQueryData`, **all components re-render** with new data
    
4. No props drilling, no context providers, no manual subscriptions
    

This replaces traditional global state patterns:

```javascript
// Traditional approach - lots of boilerplate
const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    fetchUser().then(setUser);
  }, []);
  
  return (
    <UserContext.Provider value={{ user, setUser, loading }}>
      {children}
    </UserContext.Provider>
  );
}

// React Query approach - automatic
// Just use the same queryKey wherever you need the data!
```

The `queryKey` acts as a global identifier. Any component using that key gets the same data, loading states, and updates—automatically.
