axios is a promise-based HTTP client used for making requests from both Node.js and browsers. react-query (now superseded by @tanstack/react-query) and swr are React hooks libraries that handle data fetching, caching, synchronization, and revalidation automatically. redux-query is a middleware that integrates data fetching logic directly into a Redux store, managing request state alongside application state. While axios handles the transport layer, the other three manage how that data is stored, updated, and reflected in the UI.
When building React applications, deciding how to fetch and manage server data is a critical architectural choice. axios provides the raw network capability, while react-query, redux-query, and swr offer higher-level abstractions to handle caching, loading states, and synchronization. Let's compare how they tackle common engineering problems.
axios requires you to manage the request lifecycle manually using React effects.
// axios: Manual fetch in useEffect
import axios from 'axios';
import { useEffect, useState } from 'react';
function UserProfile({ id }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios.get(`/api/users/${id}`)
.then(res => setData(res.data))
.finally(() => setLoading(false));
}, [id]);
if (loading) return <div>Loading...</div>;
return <div>{data?.name}</div>;
}
react-query uses a dedicated hook to handle fetching and caching automatically.
// react-query: useQuery hook
import { useQuery } from 'react-query';
import axios from 'axios';
function UserProfile({ id }) {
const { data, isLoading, error } = useQuery(
['user', id],
() => axios.get(`/api/users/${id}`).then(res => res.data)
);
if (isLoading) return <div>Loading...</div>;
return <div>{data?.name}</div>;
}
redux-query maps query state to props or hooks via the Redux store.
// redux-query: useReduxQuery hook
import { useReduxQuery } from 'react-redux-query';
const getUserQuery = (id) => ({
url: `/api/users/${id}`,
transform: (res) => res.data,
queryKey: ['user', id],
});
function UserProfile({ id }) {
const [{ data }, query] = useReduxQuery(getUserQuery(id));
// query.requestStatus indicates loading state
if (query.requestStatus === 'pending') return <div>Loading...</div>;
return <div>{data?.name}</div>;
}
swr provides a lightweight hook similar to React Query but with a simpler API.
// swr: useSWR hook
import useSWR from 'swr';
import axios from 'axios';
const fetcher = (url) => axios.get(url).then(res => res.data);
function UserProfile({ id }) {
const { data, error, isLoading } = useSWR(`/api/users/${id}`, fetcher);
if (isLoading) return <div>Loading...</div>;
return <div>{data?.name}</div>;
}
axios has no built-in caching mechanism.
// axios: No caching (Manual implementation required)
// Every render cycle with useEffect triggers a new GET request
useEffect(() => {
axios.get('/api/data'); // Always hits network
}, []);
react-query caches responses in memory by query key.
// react-query: Automatic caching
// Data is cached under ['todos'] key
const { data } = useQuery(['todos'], fetchTodos);
// Unmounting does not clear cache immediately
redux-query stores fetched data in the Redux state tree.
// redux-query: Redux store caching
// Data lives in redux state under entities.users
// Managed by redux-query middleware transformations
const entities = useSelector(state => state.entities);
swr caches data in memory with a built-in provider.
// swr: Built-in cache
// Cache is shared globally for the key
const { data } = useSWR('/api/data', fetcher);
// Configure revalidation time
const { data } = useSWR('/api/data', fetcher, { revalidateOnFocus: false });
axios relies on standard promise chains for mutations.
// axios: Manual mutation
const updateUser = async (id, name) => {
await axios.put(`/api/users/${id}`, { name });
// Manually update local state or reload page
setUser({ ...user, name });
};
react-query provides useMutation for side effects.
// react-query: useMutation
const mutation = useMutation(
(newData) => axios.put('/api/user', newData),
{
onSuccess: () => {
queryClient.invalidateQueries(['user']);
}
}
);
redux-query handles mutations via Redux actions.
// redux-query: Dispatch mutation
const updateUser = (id, name) => {
dispatch({
type: 'UPDATE_USER',
query: { url: `/api/users/${id}`, method: 'PUT', body: { name } }
});
};
swr uses the mutate function to update cache and server.
// swr: mutate function
import useSWR, { mutate } from 'swr';
const updateUser = async (id, name) => {
await axios.put(`/api/users/${id}`, { name });
mutate('/api/users/' + id, { ...data, name }, false);
};
axios throws errors that must be caught in try/catch blocks.
// axios: Try/Catch block
try {
await axios.get('/api/data');
} catch (err) {
console.error(err.response?.data);
// Manual UI error state update
}
react-query returns error objects directly from the hook.
// react-query: Error from hook
const { error } = useQuery(['data'], fetchData);
if (error) return <div>Error: {error.message}</div>;
redux-query stores error state in the Redux store.
// redux-query: Error from state
const [{ isErrored }] = useReduxQuery(myQuery);
if (isErrored) return <div>Request Failed</div>;
swr returns error objects similar to React Query.
// swr: Error from hook
const { error } = useSWR('/api/data', fetcher);
if (error) return <div>Failed to load</div>;
While the approaches differ, these libraries share common goals and underlying technologies.
axios (which is framework-agnostic), all provide React-specific hooks.// Shared: Functional Component Pattern
function MyComponent() {
// All libraries (except raw axios) provide hooks here
return <div>Data Display</div>;
}
axios returns promises; others wrap them for state management.// Shared: Promise Resolution
// axios returns promise
axios.get('/url').then(res => ...);
// Others accept promise-returning functions
useQuery('key', () => axios.get('/url').then(res => res.data));
// Shared: Error Handling
// axios
.catch(err => handleError(err));
// react-query/swr
if (error) showErrorMessage();
// axios: Interceptors
axios.interceptors.request.use(config => {
config.headers.Auth = getToken();
return config;
});
// swr: Custom Fetcher
const fetcher = (url) => axios.get(url, { headers: { Auth: getToken() } });
| Feature | axios | react-query | redux-query | swr |
|---|---|---|---|---|
| Type | HTTP Client | Data Hook | Redux Middleware | Data Hook |
| Caching | β None | β Automatic | β Redux Store | β Automatic |
| Boilerplate | High (Manual) | Low | High (Redux) | Very Low |
| React Integration | None (Manual) | Deep (Hooks) | Deep (Connect/Hooks) | Deep (Hooks) |
| Mutation Tools | Manual Promises | useMutation | Dispatch Actions | mutate Function |
| Bundle Weight | Lightweight | Heavier | Heavy (Redux + Lib) | Lightweight |
axios is the foundation π§± β it handles the network traffic but leaves state management to you. Use it when you need raw control or are not using React.
react-query is the powerhouse ποΈ β it manages complex server state with robust tools for caching, mutations, and background sync. Best for large-scale apps.
redux-query is the integrator π β it fits data fetching into an existing Redux flow. Best for legacy apps already committed to Redux.
swr is the sprinter π β it offers fast setup and lightweight caching. Best for Next.js projects or simpler data needs.
Final Thought: If you are starting a new React project today, react-query (TanStack Query) or swr are the modern standards. axios remains essential as the underlying transport for both. Reserve redux-query for maintaining existing Redux-heavy codebases.
Choose redux-query if your application already relies heavily on Redux for all state management and you want data fetching to follow the same unidirectional data flow. It suits legacy enterprise apps where introducing a new data caching library would create too much architectural fragmentation.
Choose axios if you need a low-level HTTP client for non-React environments or want full manual control over request lifecycle without built-in caching. It is ideal for simple scripts, backend Node.js services, or when you plan to build your own caching layer on top of raw promises.
Choose react-query (or its successor @tanstack/react-query) if you need powerful caching, background updates, and robust mutation handling for complex applications. It is the best fit for dashboards or data-heavy apps where server state management needs to be separated from client UI state.
Choose swr if you want a lightweight, fast-to-setup data fetching library with automatic revalidation and focus on simplicity. It is perfect for Next.js projects or smaller applications where you need caching benefits without the heavier configuration of React Query.
A library for managing network state in Redux.
Brought to you by Amplitude Engineering. We're hiring!