| name | SWR Data Fetching |
| description | This skill should be used when the user asks to "setup SWR", "data fetching hooks", "cache management", "optimistic updates", or works with SWR library for React data fetching in x-project-v2 frontend. |
| version | 0.1.0 |
SWR Data Fetching for x-project-v2
SWR is a React Hooks library for data fetching with caching, revalidation, and optimistic updates.
Installation
pnpm add swr
Directory Structure
lib/
├── api-client.ts # API client singleton
├── api-types.ts # TypeScript interfaces
└── hooks/
├── use-lists.ts # Lists data fetching
└── use-tasks.ts # Tasks data fetching
API Client Pattern
import { getApiBase } from "./api-config";
import type { List, Task, ListsResponse, TasksResponse } from "./api-types";
class ApiClient {
private get base() {
return getApiBase();
}
private async fetch<T>(path: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${this.base}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: "Request failed" }));
throw new Error(error.error || "Request failed");
}
return response.json();
}
async getLists(): Promise<ListsResponse> {
return this.fetch<ListsResponse>("/lists");
}
async createList(data: { name: string }): Promise<List> {
return this.fetch<List>("/lists", {
method: "POST",
body: JSON.stringify(data),
});
}
async updateList(id: string, data: { name: string }): Promise<List> {
return this.fetch<List>(`/lists/${id}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
async deleteList(id: string): Promise<void> {
await this.fetch(`/lists/${id}`, { method: "DELETE" });
}
async getTasks(listId: string): Promise<TasksResponse> {
return this.fetch<TasksResponse>(`/lists/${listId}/tasks`);
}
async createTask(listId: string, data: { title: string }): Promise<Task> {
return this.fetch<Task>(`/lists/${listId}/tasks`, {
method: "POST",
body: JSON.stringify(data),
});
}
async toggleTaskCompleted(id: string): Promise<Task> {
return this.fetch<Task>(`/tasks/${id}/toggle-completed`, {
method: "POST",
});
}
}
export const api = new ApiClient();
SWR Hook Pattern
useLists Hook
import useSWR, { mutate } from "swr";
import { api } from "@/lib/api-client";
import type { List } from "@/lib/api-types";
const LISTS_KEY = "lists";
export function useLists() {
const { data, error, isLoading, mutate: revalidate } = useSWR(
LISTS_KEY,
() => api.getLists(),
{ revalidateOnFocus: false }
);
const createList = async (data: { name: string }) => {
const list = await api.createList(data);
revalidate(
(current) => ({ lists: [...(current?.lists || []), list] }),
{ revalidate: true }
);
return list;
};
const updateList = async (id: string, data: { name: string }) => {
const list = await api.updateList(id, data);
revalidate(
(current) => ({
lists: current?.lists.map((l) => (l.id === id ? list : l)) || [],
}),
{ revalidate: true }
);
return list;
};
const deleteList = async (id: string) => {
await api.deleteList(id);
revalidate(
(current) => ({
lists: current?.lists.filter((l) => l.id !== id) || [],
}),
{ revalidate: true }
);
};
return {
lists: data?.lists || [],
isLoading,
error,
createList,
updateList,
deleteList,
mutate: revalidate,
};
}
useTasks Hook
import useSWR, { mutate } from "swr";
import { api } from "@/lib/api-client";
import type { Task, CreateTaskRequest } from "@/lib/api-types";
export function getTasksKey(listId: string) {
return `tasks-${listId}`;
}
export function useTasks(listId: string | null) {
const { data, error, isLoading, mutate: revalidate } = useSWR(
listId ? getTasksKey(listId) : null,
() => (listId ? api.getTasks(listId) : null),
{ revalidateOnFocus: false }
);
const createTask = async (data: CreateTaskRequest) => {
if (!listId) return null;
const task = await api.createTask(listId, data);
revalidate(
(current) => ({ tasks: [...(current?.tasks || []), task] }),
{ revalidate: true }
);
return task;
};
const toggleCompleted = async (taskId: string) => {
const task = await api.toggleTaskCompleted(taskId);
revalidate(
(current) => ({
tasks: current?.tasks.map((t) =>
t.id === taskId ? task : t
) || [],
}),
{ revalidate: false }
);
return task;
};
const deleteTask = async (taskId: string) => {
await api.deleteTask(taskId);
revalidate(
(current) => ({
tasks: current?.tasks.filter((t) => t.id !== taskId) || [],
}),
{ revalidate: true }
);
};
return {
tasks: data?.tasks || [],
isLoading,
error,
createTask,
toggleCompleted,
deleteTask,
mutate: revalidate,
};
}
Usage in Components
import { useLists } from "@/lib/hooks/use-lists";
export function ListSidebar() {
const { lists, isLoading, createList, deleteList } = useLists();
if (isLoading) return <div>Loading...</div>;
return (
<div>
{lists.map((list) => (
<div key={list.id}>
{list.name}
<button onClick={() => deleteList(list.id)}>Delete</button>
</div>
))}
<button onClick={() => createList({ name: "New List" })}>
Add List
</button>
</div>
);
}
import { useTasks } from "@/lib/hooks/use-tasks";
interface TaskListProps {
listId: string;
}
export function TaskList({ listId }: TaskListProps) {
const { tasks, isLoading, createTask, toggleCompleted } = useTasks(listId);
if (isLoading) return <div>Loading tasks...</div>;
return (
<div>
{tasks.map((task) => (
<div key={task.id}>
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleCompleted(task.id)}
/>
{task.title}
</div>
))}
<button onClick={() => createTask({ title: "New Task" })}>
Add Task
</button>
</div>
);
}
Cache Invalidation
import { mutate } from "swr";
mutate("lists");
mutate(`tasks-${listId}`);
mutate((key) => typeof key === "string" && key.startsWith("tasks-"));
Optimistic Updates
const toggleImportant = async (taskId: string) => {
mutate(
getTasksKey(listId),
(current) => ({
tasks: current?.tasks.map((t) =>
t.id === taskId ? { ...t, important: !t.important } : t
) || [],
}),
{ revalidate: false }
);
await api.toggleTaskImportant(taskId);
};
SWR Configuration
import { SWRConfig } from "swr";
function App() {
return (
<SWRConfig
value={{
refreshInterval: 30000, // Auto refresh every 30s
revalidateOnFocus: true, // Refresh when window regains focus
revalidateOnReconnect: true, // Refresh when network reconnects
errorRetryCount: 3, // Retry failed requests 3 times
}}
>
<YourApp />
</SWRConfig>
);
}
Best Practices
- Use consistent cache keys - Define keys as constants or functions
- Revalidate after mutations - Always call
mutate() after create/update/delete
- Handle loading states - Check
isLoading before rendering
- Handle errors - Check
error and show error UI
- Optimistic updates - Use for toggle operations to feel snappy
- Conditional fetching - Pass
null as key to skip fetching
Common Patterns
const { data } = useSWR(
shouldFetch ? "lists" : null,
() => api.getLists()
);
const { data: list } = useSWR("selected-list", getSelectedList);
const { data: tasks } = useSWR(
list ? `tasks-${list.id}` : null,
() => api.getTasks(list!.id)
);
const { data } = useSWR("lists", () => api.getLists(), {
refreshInterval: 5000,
});