React API Patterns Core Knowledge
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: react for comprehensive documentation.
When NOT to Use This Skill
Skip this skill when:
-
Using TanStack Query exclusively (use state-tanstack-query )
-
Working with GraphQL (use Apollo Client or urql)
-
Building non-React applications
-
Server Components handle data fetching (Next.js App Router)
-
Need advanced caching beyond SWR (use state-tanstack-query )
Custom useFetch Hook
import { useState, useEffect, useCallback } from 'react';
interface UseFetchState<T> { data: T | null; loading: boolean; error: Error | null; }
interface UseFetchOptions { immediate?: boolean; }
export function useFetch<T>( url: string, options?: RequestInit & UseFetchOptions ) { const [state, setState] = useState<UseFetchState<T>>({ data: null, loading: options?.immediate !== false, error: null, });
const execute = useCallback(async () => { setState(prev => ({ ...prev, loading: true, error: null }));
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setState({ data, loading: false, error: null });
return data;
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error as Error,
}));
throw error;
}
}, [url, options]);
useEffect(() => { if (options?.immediate !== false) { execute(); } }, [execute, options?.immediate]);
return { ...state, refetch: execute }; }
// Usage
function UserProfile({ id }: { id: string }) {
const { data: user, loading, error, refetch } = useFetch<User>(
/api/users/${id}
);
if (loading) return <Spinner />; if (error) return <Error message={error.message} onRetry={refetch} />; return <div>{user?.name}</div>; }
SWR (Stale-While-Revalidate)
npm install swr
Basic Usage
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(res => res.json());
function UserProfile({ id }: { id: string }) {
const { data, error, isLoading, mutate } = useSWR<User>(
/api/users/${id},
fetcher
);
if (isLoading) return <Spinner />; if (error) return <Error />; return <div>{data?.name}</div>; }
Global Configuration
import { SWRConfig } from 'swr';
const fetcher = async (url: string) => {
const res = await fetch(url, {
headers: { Authorization: Bearer ${getToken()} },
});
if (!res.ok) throw new Error('API Error');
return res.json();
};
function App() { return ( <SWRConfig value={{ fetcher, revalidateOnFocus: true, revalidateOnReconnect: true, dedupingInterval: 2000, errorRetryCount: 3, onError: (error) => console.error(error), }} > <MyApp /> </SWRConfig> ); }
Mutations with useSWRMutation
import useSWRMutation from 'swr/mutation';
async function createUser(url: string, { arg }: { arg: CreateUserDto }) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(arg), }); return res.json(); }
function CreateUserForm() { const { trigger, isMutating } = useSWRMutation('/api/users', createUser);
const handleSubmit = async (data: CreateUserDto) => { try { await trigger(data); // Success } catch (error) { // Error } };
return <form onSubmit={handleSubmit}>...</form>; }
Optimistic Updates
function UserList() { const { data, mutate } = useSWR<User[]>('/api/users', fetcher);
const deleteUser = async (id: string) => { // Optimistic update const optimisticData = data?.filter(u => u.id !== id); mutate(optimisticData, { revalidate: false });
try {
await fetch(`/api/users/${id}`, { method: 'DELETE' });
mutate(); // Revalidate after success
} catch {
mutate(data); // Rollback on error
}
};
return ( <ul> {data?.map(user => ( <li key={user.id}> {user.name} <button onClick={() => deleteUser(user.id)}>Delete</button> </li> ))} </ul> ); }
React Suspense with use()
// React 18+ with use() hook import { use, Suspense } from 'react';
// Create promise outside component const userPromise = fetch('/api/users/1').then(res => res.json());
function UserProfile() { const user = use(userPromise); return <div>{user.name}</div>; }
function App() { return ( <Suspense fallback={<Spinner />}> <UserProfile /> </Suspense> ); }
Suspense with SWR
import useSWR from 'swr';
function UserProfile({ id }: { id: string }) {
const { data } = useSWR<User>(/api/users/${id}, fetcher, {
suspense: true,
});
// data is guaranteed to exist (no loading state) return <div>{data.name}</div>; }
function App() { return ( <Suspense fallback={<Spinner />}> <UserProfile id="1" /> </Suspense> ); }
Error Boundaries
import { Component, ReactNode } from 'react';
interface Props { children: ReactNode; fallback?: ReactNode; onError?: (error: Error) => void; }
interface State { hasError: boolean; error: Error | null; }
export class ApiErrorBoundary extends Component<Props, State> { state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; }
componentDidCatch(error: Error) { this.props.onError?.(error); }
reset = () => { this.setState({ hasError: false, error: null }); };
render() { if (this.state.hasError) { return this.props.fallback ?? ( <div> <h2>Something went wrong</h2> <button onClick={this.reset}>Try again</button> </div> ); } return this.props.children; } }
// Usage function App() { return ( <ApiErrorBoundary fallback={<ErrorFallback />} onError={(error) => logError(error)} > <Suspense fallback={<Spinner />}> <UserProfile /> </Suspense> </ApiErrorBoundary> ); }
Real-Time Updates
Server-Sent Events (SSE)
function useSSE<T>(url: string) { const [data, setData] = useState<T | null>(null); const [error, setError] = useState<Error | null>(null);
useEffect(() => { const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
setData(JSON.parse(event.data));
};
eventSource.onerror = () => {
setError(new Error('SSE connection failed'));
eventSource.close();
};
return () => eventSource.close();
}, [url]);
return { data, error }; }
// Usage function Notifications() { const { data: notification } = useSSE<Notification>('/api/notifications'); return notification ? <Toast message={notification.message} /> : null; }
WebSocket Hook
function useWebSocket<T>(url: string) { const [data, setData] = useState<T | null>(null); const [isConnected, setIsConnected] = useState(false); const wsRef = useRef<WebSocket | null>(null);
useEffect(() => { const ws = new WebSocket(url); wsRef.current = ws;
ws.onopen = () => setIsConnected(true);
ws.onclose = () => setIsConnected(false);
ws.onmessage = (event) => setData(JSON.parse(event.data));
return () => ws.close();
}, [url]);
const send = useCallback((message: unknown) => { wsRef.current?.send(JSON.stringify(message)); }, []);
return { data, isConnected, send }; }
// Usage function Chat() { const { data: message, send, isConnected } = useWebSocket<Message>( 'wss://api.example.com/chat' );
const handleSend = (text: string) => { send({ type: 'message', text }); };
return ( <div> {isConnected ? 'Connected' : 'Connecting...'} {message && <Message data={message} />} </div> ); }
Loading States
Skeleton Loading
function UserCardSkeleton() { return ( <div className="animate-pulse"> <div className="h-12 w-12 bg-gray-200 rounded-full" /> <div className="h-4 w-32 bg-gray-200 mt-2 rounded" /> <div className="h-3 w-24 bg-gray-200 mt-1 rounded" /> </div> ); }
function UserCard({ id }: { id: string }) {
const { data, isLoading } = useSWR<User>(/api/users/${id}, fetcher);
if (isLoading) return <UserCardSkeleton />; return ( <div> <img src={data?.avatar} /> <h3>{data?.name}</h3> <p>{data?.email}</p> </div> ); }
Anti-Patterns
Anti-Pattern Why It's Bad Correct Approach
Fetching in useEffect without cleanup Race conditions Use AbortController
Not handling loading/error states Poor UX Always show loading/error UI
Fetching in render Infinite loops Use useEffect or Suspense
No request deduplication Duplicate requests Use SWR or TanStack Query
Hardcoded API URLs Hard to maintain Use environment variables
Not retrying failed requests Poor resilience Add retry logic
Quick Troubleshooting
Issue Likely Cause Solution
Infinite fetch loop Fetching in render Move to useEffect
Stale data shown No revalidation Configure SWR revalidation
Race condition Multiple rapid requests Use AbortController
Memory leak warning Updating unmounted component Check mounted state or cleanup
Duplicate requests No deduplication Use SWR or caching layer
CORS error Backend not configured Configure CORS headers
Production Readiness
Checklist
-
Global fetcher with auth headers
-
Error boundary for API errors
-
Loading skeletons for better UX
-
Optimistic updates for mutations
-
Request deduplication
-
Automatic retry on failure
-
Revalidation strategy defined
-
Real-time updates where needed
-
Proper cleanup on unmount
Reference Documentation
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: react for comprehensive documentation.
-
Custom Hooks
-
Suspense
-
Error Boundaries
-
SWR
-
Real-Time