Dax's Blog

F2E, Full-Stack To Indie Hacker

SWR: Concise and Pragmatic Data Fetching for React

SWR is a modern data fetching library for React, featuring a minimalist API with built-in caching, revalidation, and request deduplication. Leveraging a simple React Hook, it ensures your UI remains fast, consistent, and always up-to-date.

1. Comparing Code Volume Across Solutions

Fetching data from backends while maintaining global state consistency is a core requirement in React. Let’s take the /api/user endpoint as an example to evaluate the implementation costs of different approaches.

1.1 Fetch + Zustand

This approach requires manual management of loading states, errors, and data storage, resulting in more fragmented and boilerplate-heavy logic.

useUserStore.ts
import { useEffect } from 'react';
import { create } from 'zustand';
export const useUserStore = create((set, get) => ({
user: null,
isLoading: false,
error: null,
fetchUser: async () => {
const { user, isLoading } = get();
if (user || isLoading) return;
set({ isLoading: true, error: null });
try {
const res = await fetch('/api/user');
if (!res.ok) throw new Error('Failed to fetch user info');
const data = await res.json();
set({ user: data, isLoading: false });
} catch (err) {
set({ error: err.message, isLoading: false });
}
},
}));

UserProfile.tsx
export function UserProfile() {
const { user, isLoading, error, fetchUser } = useUserStore();
useEffect(() => {
fetchUser();
}, [fetchUser]);
if (isLoading && !user) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user?.name}</div>;
}

1.2 React Query

“Powerful, yet comes with a higher configuration overhead. You need to define a QueryClient, wrap the app in a Provider, and manually declare queryKeys. Furthermore, the documentation can feel somewhat overwhelming and fragmented.

APIProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
refetchOnWindowFocus: false,
},
},
});
export function APIProvider({ children }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}

useUser.tsx
import { useQuery } from '@tanstack/react-query';
const fetchUser = async () => {
const response = await fetch('/api/user');
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
};
export function useUser() {
return useQuery({
queryKey: ['user'],
queryFn: fetchUser,
});
}

UserProfile.tsx
export function UserProfile() {
const { data: user, isLoading, error } = useUser();
if (isLoading) return <div>Loading user info...</div>;
if (error) return <div>Failed to load: {error.message}</div>;
return (
<div>
<h1>User Info</h1>
<p>Username: {user?.name}</p>
<p>Email: {user?.email}</p>
</div>
);
}

1.3 SWR

Ultimate simplicity.

useUser.ts
import useSWR from 'swr';
const fetcher = async (url) => {
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
};
export function useUser() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
return {
user: data,
isLoading,
error
};
}

UserProfile.tsx
export function UserProfile() {
const { user, isLoading, error } = useUser();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user?.name}</div>;
}

Conclusion: To achieve the same functionality, SWR offers the lowest cognitive load and minimal boilerplate. While React Query is undeniably powerful, SWR proves to be more than sufficient—and far more streamlined—for most everyday requirements.

2. Design Philosophy: Reaching the Essence

2.1 Concise and Elegant Taste

SWR’s API design is spot on. This “less is more” aesthetic reflects the developers’ deep expertise and refined taste in API ergonomics.

2.2 The Unique “URL as Key” Design

  • Unique Resource Identifier: In Zustand, we manually name storage variables; in React Query, we maintain queryKey arrays. SWR, however, defaults to using the URL as the cache identifier.
  • Back to Basics: URL stands for Uniform Resource Locator. It is born to be the unique identifier for a resource, making it the perfect candidate for a cache key.

3.Mutations: Flexible Caching Strategies

Unlike typical request libraries, SWR enables caching strategies by default. Don’t worry—this isn’t a “cognitive load” but rather a way to achieve more flexible data handling with less code in common scenarios.

In practice, useSWR for data fetching is mostly “set it and forget it.” The real power lies in handling mutations with useSWRMutation. First, let’s understand two key configurations:

  • populateCache: Whether to update the local cache directly with the response from the mutation request.
  • revalidate: Whether to trigger a fresh GET request to re-verify the data once the mutation is complete.

3.1 Revalidation Mode

  • Configuration: revalidate: true | populateCache: false
  • Workflow: PATCH Success -> Trigger GET Request -> Update UI
  • Use Case: This mode introduces the most latency but is the most reliable. It is ideal for scenarios with complex backend logic (e.g., where a single update triggers side effects in other data fields).
const { trigger, isMutating } = useSWRMutation('/api/user', updateUserFetcher, {
populateCache: false,
revalidate: true,
});

3.2 Populate Cache Mode

  • Configuration: revalidate: false | populateCache: true
  • Workflow: PATCH Success (returns new data) -> Update UI using the PATCH response data
  • Use Case: A balanced choice that requires only a single network round-trip. It offers a middle ground between speed and consistency, making it suitable for standard CRUD operations.
function Profile() {
const { trigger } = useSWRMutation('/api/user', updateUserFetcher, {
populateCache: true,
revalidate: false,
});
return <button onClick={() => trigger({ name: 'New Name' })}>Quick Update</button>;
}

3.3 Optimistic Updates

  • Configuration: Uses optimisticData | populateCache: true | revalidate: false | rollbackOnError: true
  • Workflow: Immediate UI update upon interaction -> Initiate PATCH request -> Finalize on success or rollback on failure.
  • Use Case: Provides the fastest perceived response time. It is ideal for high-interaction scenarios like “Likes” or comments, where real-time feedback is critical.
const { trigger, isMutating } = useSWRMutation('/api/user', updateUserFetcher, {
optimisticData: (currentCache) => ({
...currentCache,
...currentUser,
status: 'Saving...'
}),
populateCache: true,
rollbackOnError: true,
revalidate: false,
});

3.4 Optimistic Updates

  • Configuration: revalidate: false | populateCache: false
  • Workflow: Initiate DELETE or UPDATE request -> UI remains unchanged with no cache updates or revalidation.
  • Use Case: Ideal for background operations that don’t require immediate visual feedback on the current UI, such as deletions, telemetry, or event tracking.
const { trigger, isMutating } = useSWRMutation('/api/user', deleteUserFetcher, {
populateCache: false,
revalidate: false
});
const handleDelete = async () => {
if (window.confirm('Are you sure you want to delete your account?')) {
try {
await trigger();
alert('Account deleted in the background (silent mode).');
} catch (e) {
alert('Deletion failed');
}
}
};

4. Conclusion

SWR is a React data-fetching Hook that strikes a perfect balance between simplicity and power. It is not only lightweight and practical but also deepens one’s technical understanding of Uniform Resource Locators (URLs).

When it comes to data fetching and caching, tools that require tedious Key configurations treat the request URL merely as a means to an end. In contrast, SWR treats the URL as the resource itself, reflecting a higher level of design maturity and taste. This is reminiscent of how Unix abstracts diverse hardware devices as “files,” or how Next.js ingeniously maps the file system to URLs. Truly great design often appears simple because it reaches the very essence of the problem.