React Query vs Redux: Server-State and Client-State Management in React

React Query (TanStack Query) specializes in declarative server-state management with automatic caching and synchronization, while Redux provides a predictable client-state container for complex UI logic, and many production applications use both libraries together.

When architecting state management for React applications, understanding the distinction between server-derived state and client-derived state is essential. While the facebook/react repository includes Redux integration examples in its fixtures for testing purposes, React Query operates as an external library that focuses exclusively on asynchronous server-state synchronization.

Core Architectural Differences

Server State vs Client State

React Query treats server state as the primary concern. It manages data that originates from an external API, handling the entire lifecycle of asynchronous operations including loading states, error handling, caching, and background synchronization. The library assumes the server is the source of truth.

Redux, as implemented in examples like fixtures/nesting/src/store.js within the React repository, manages client-side state. This includes UI state (theme preferences, modal visibility), business logic state, and any data that must persist across components without necessarily originating from a server request.

Data Synchronization Strategies

React Query provides automatic synchronization features out-of-the-box. When configured with refetchOnWindowFocus: true or interval polling, the library handles refetching data when the application regains focus or based on time intervals. This eliminates manual synchronization logic.

Redux requires manual dispatch patterns to keep data synchronized. After a mutation, developers must explicitly dispatch actions to update the store, as shown in the test files like packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js where Redux integration requires specific hook inspection patterns.

Boilerplate and Developer Experience

React Query minimizes boilerplate through hooks like useQuery and useMutation. Setup requires only wrapping the application in a QueryClientProvider:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

Redux demands more initial configuration. Based on the pattern in fixtures/nesting/src/store.js, developers must create a store, define reducers, and potentially configure middleware:

// fixtures/nesting/src/store.js pattern
import { createStore } from 'redux';

const initialState = { todos: [] };

function todoReducer(state = initialState, action) {
  switch (action.type) {
    case 'SET_TODOS':
      return { ...state, todos: action.payload };
    default:
      return state;
  }
}

export const store = createStore(todoReducer);

Caching and Performance

Automatic Caching in React Query

React Query implements query key-based caching with configurable staleTime. The library automatically deduplicates requests with identical query keys and serves cached data while fetching fresh data in the background:

const { data, isLoading } = useQuery(
  ['todos'],           // query key for caching
  fetchTodos,
  {
    staleTime: 5 * 60 * 1000,  // 5 minutes
    cacheTime: 10 * 60 * 1000  // garbage collection time
  }
);

Manual Implementation in Redux

Redux provides no built-in caching mechanisms. To implement similar behavior, developers must manually track loading states and memoize selectors using additional libraries like Reselect, or implement custom logic to prevent duplicate fetches.

Code Examples: Fetching Data

React Query Implementation

This example demonstrates declarative data fetching with automatic loading and error states:

import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import axios from 'axios';

const queryClient = new QueryClient();

function Todos() {
  const { data, error, isLoading } = useQuery(
    ['todos'],
    async () => {
      const res = await axios.get('/api/todos');
      return res.data;
    },
    {
      staleTime: 5 * 60 * 1000,
      refetchOnWindowFocus: true,
    }
  );

  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>Error loading todos.</p>;

  return (
    <ul>
      {data.map((todo: any) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  );
}

Redux Implementation

This example shows the equivalent pattern using Redux, based on structures found in the React repository fixtures:

// store.ts
import { createStore } from 'redux';

interface State {
  todos: any[];
  loading: boolean;
  error: string | null;
}

const initialState: State = {
  todos: [],
  loading: false,
  error: null
};

function todoReducer(state = initialState, action: any): State {
  switch (action.type) {
    case 'FETCH_TODOS_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_TODOS_SUCCESS':
      return { ...state, loading: false, todos: action.payload };
    case 'FETCH_TODOS_ERROR':
      return { ...state, loading: false, error: action.error };
    default:
      return state;
  }
}

export const store = createStore(todoReducer);
// Todos.tsx
import { useEffect } from 'react';
import { useSelector, useDispatch, Provider } from 'react-redux';
import { store } from './store';
import axios from 'axios';

function Todos() {
  const { todos, loading, error } = useSelector((state: any) => state);
  const dispatch = useDispatch();

  useEffect(() => {
    async function loadTodos() {
      dispatch({ type: 'FETCH_TODOS_START' });
      try {
        const res = await axios.get('/api/todos');
        dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: res.data });
      } catch (err) {
        dispatch({ type: 'FETCH_TODOS_ERROR', error: 'Failed to load' });
      }
    }
    loadTodos();
  }, [dispatch]);

  if (loading) return <p>Loading…</p>;
  if (error) return <p>{error}</p>;

  return (
    <ul>
      {todos.map((todo: any) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

export default function App() {
  return (
    <Provider store={store}>
      <Todos />
    </Provider>
  );
}

When to Use React Query vs Redux

Choose React Query when:

  • Your application primarily fetches, caches, and synchronizes server data
  • You need automatic background refetching, pagination, or optimistic updates
  • You want to eliminate loading state boilerplate
  • The server is the source of truth for your data

Choose Redux when:

  • You have complex client-side state unrelated to server data (themes, UI modes, complex forms)
  • You need deterministic state transitions with time-travel debugging
  • Your application requires complex middleware chains for side effects
  • You are building a library where consumers need predictable state containers

Many production applications, including those tested in the React repository, combine both approaches: Redux manages global UI state while React Query handles all server communication.

React Repository References

The facebook/react repository contains several references to Redux integration patterns:

These files illustrate that while React Query operates outside the core React repository, Redux has established integration patterns tested within React's official fixture suite.

Summary

  • React Query (TanStack Query) is a server-state management library that provides automatic caching, background refetching, and declarative data fetching through hooks like useQuery and useMutation.

  • Redux is a client-state container that requires manual action dispatching and reducer logic, offering deterministic state transitions and time-travel debugging for complex UI state.

  • The facebook/react repository includes Redux integration examples in fixtures/nesting/src/store.js and test files like ReactHooksInspectionIntegration-test.js, while React Query exists as an external specialized library.

  • Most modern React applications benefit from using both libraries together: React Query for server data synchronization and Redux (or Redux Toolkit) for global client-side state.

Frequently Asked Questions

Can I use React Query and Redux together in the same application?

Yes, many production applications use both libraries simultaneously. React Query handles server-state fetching, caching, and synchronization, while Redux manages client-only state like UI themes, modal visibility, or complex form state. This separation of concerns allows each tool to excel at its specific domain without conflict.

Does React Query replace Redux entirely?

No, React Query does not replace Redux for client-state management. While React Query can eliminate the need for Redux when handling server data, Redux remains essential for complex client-side business logic, state that must persist across sessions, or applications requiring deterministic state transitions with middleware chains.

Which library has better TypeScript support?

Both libraries offer excellent TypeScript support, but with different ergonomic approaches. React Query provides type inference through its hook APIs, automatically inferring types from fetch functions. Redux Toolkit, the modern standard for Redux development, includes advanced type utilities for actions and reducers, though it requires more explicit type definitions for store configuration.

How do the testing approaches differ between React Query and Redux?

Testing Redux involves mocking the store, dispatching actions, and asserting state changes in reducers, as demonstrated in packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js within the React repository. React Query testing focuses on mocking API responses and verifying hook behaviors using utilities like renderHook from React Testing Library, with the library handling caching and background updates automatically.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →