Category: guide
Fetch API Patterns — Data Fetching, AbortController, Retry
Patterns สำหรับ fetch ข้อมูล: timeout, retry, AbortController, error handling, caching headers, streaming response
สารบัญ
Fetch พื้นฐาน
// GET
const res = await fetch('/api/data');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
// POST
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' }),
});
Error Handling ที่ถูกต้อง
// ❌ fetch ไม่ throw สำหรับ 4xx/5xx — ต้อง check res.ok เอง
const res = await fetch('/api/data');
const data = await res.json(); // อาจเป็น error response โดยไม่รู้
// ✓ check status ก่อนเสมอ
async function fetchJSON<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, init);
if (!res.ok) {
const error = await res.text().catch(() => `HTTP ${res.status}`);
throw new Error(`Fetch failed ${res.status}: ${error}`);
}
return res.json() as Promise<T>;
}
interface User { id: number; name: string }
const user = await fetchJSON<User>('/api/users/1');
AbortController — Cancel Request
// Cancel เมื่อ component unmount
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name === 'AbortError') return; // ถูก cancel — ignore
throw err;
});
// Cancel
controller.abort();
// Timeout pattern
function fetchWithTimeout(url: string, ms: number, init?: RequestInit) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
return fetch(url, { ...init, signal: controller.signal })
.finally(() => clearTimeout(timer));
}
try {
const res = await fetchWithTimeout('/api/slow', 5000);
const data = await res.json();
} catch (err) {
if ((err as Error).name === 'AbortError') {
console.error('Request timed out');
}
}
Retry Pattern
async function fetchWithRetry<T>(
url: string,
options: RequestInit & {
retries?: number;
retryDelay?: number;
retryOn?: number[];
} = {},
): Promise<T> {
const {
retries = 3,
retryDelay = 1000,
retryOn = [429, 500, 502, 503, 504],
...fetchOptions
} = options;
let lastError: Error;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const res = await fetch(url, fetchOptions);
if (!res.ok && retryOn.includes(res.status) && attempt < retries) {
// Exponential backoff
const delay = retryDelay * 2 ** attempt;
await new Promise((r) => setTimeout(r, delay));
continue;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
} catch (err) {
lastError = err as Error;
if (attempt < retries && (err as Error).name !== 'AbortError') {
await new Promise((r) => setTimeout(r, retryDelay * 2 ** attempt));
continue;
}
throw err;
}
}
throw lastError!;
}
const data = await fetchWithRetry<User>('/api/users/1', {
retries: 3,
retryDelay: 500,
retryOn: [429, 500, 503],
});
Caching Headers
// No cache — ได้ข้อมูลใหม่เสมอ
fetch('/api/data', { cache: 'no-store' })
// Use cache, revalidate in background (stale-while-revalidate)
fetch('/api/data', { cache: 'no-cache' }) // always check server
// ใช้ cache ถ้ามี (default)
fetch('/api/data', { cache: 'default' })
// Force cache, ไม่ไปถามเซิร์ฟเวอร์เลย
fetch('/api/data', { cache: 'force-cache' })
// Request headers สำหรับ conditional fetch
const lastModified = localStorage.getItem('data-modified');
const res = await fetch('/api/data', {
headers: lastModified ? { 'If-Modified-Since': lastModified } : {},
});
if (res.status === 304) {
// ไม่มีอะไรใหม่ ใช้ cached data
} else {
const data = await res.json();
localStorage.setItem('data-modified', res.headers.get('Last-Modified') ?? '');
}
Streaming Response
// อ่าน ReadableStream ทีละ chunk
async function streamResponse(url: string) {
const res = await fetch(url);
if (!res.body) throw new Error('No body');
const reader = res.body.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
result += chunk;
console.log('Chunk:', chunk); // process แต่ละ chunk ทันที
}
return result;
}
// Server-Sent Events (SSE) / AI streaming
async function* streamSSE(url: string) {
const res = await fetch(url, {
headers: { Accept: 'text/event-stream' },
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) return;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (line.startsWith('data: ')) {
yield JSON.parse(line.slice(6));
}
}
}
}
for await (const event of streamSSE('/api/stream')) {
console.log(event);
}
FormData Upload
// Upload file
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('description', 'My file');
const res = await fetch('/api/upload', {
method: 'POST',
body: formData, // ไม่ต้องตั้ง Content-Type — browser จะ set boundary เอง
});
// Progress tracking ต้องใช้ XMLHttpRequest (fetch ไม่มี upload progress)
Parallel Fetching
// ✓ รันพร้อมกัน — เร็วกว่า await แต่ละตัว
const [users, posts, comments] = await Promise.all([
fetchJSON<User[]>('/api/users'),
fetchJSON<Post[]>('/api/posts'),
fetchJSON<Comment[]>('/api/comments'),
]);
// ✓ allSettled — ไม่ cancel ถ้าตัวใดตัวหนึ่ง fail
const results = await Promise.allSettled([
fetchJSON('/api/a'),
fetchJSON('/api/b'),
]);
const data = results
.filter((r): r is PromiseFulfilledResult<unknown> => r.status === 'fulfilled')
.map((r) => r.value);
// ❌ sequential — ช้า (รอทีละตัว)
const users = await fetchJSON('/api/users');
const posts = await fetchJSON('/api/posts');
Simple Client-side Cache
const cache = new Map<string, { data: unknown; ts: number }>();
async function cachedFetch<T>(url: string, ttl = 60_000): Promise<T> {
const cached = cache.get(url);
if (cached && Date.now() - cached.ts < ttl) {
return cached.data as T;
}
const data = await fetchJSON<T>(url);
cache.set(url, { data, ts: Date.now() });
return data;
}
// ใช้:
const users = await cachedFetch<User[]>('/api/users', 30_000); // cache 30 วินาที
Authentication Headers
function createClient(baseUrl: string, getToken: () => string | null) {
return {
async get<T>(path: string): Promise<T> {
const token = getToken();
return fetchJSON<T>(`${baseUrl}${path}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
},
async post<T>(path: string, body: unknown): Promise<T> {
const token = getToken();
return fetchJSON<T>(`${baseUrl}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(body),
});
},
};
}
const api = createClient('https://api.example.com', () => localStorage.getItem('token'));
const users = await api.get<User[]>('/users');