Truy cập axios-http.com/docs. Tập trung vào: creating instances, interceptors, request config, response schema, error handling. Không cần đọc hết — focus vào phần Instance, Interceptors, và Cancellation.
Tạo một file api/client.ts trong project React Native. Config base URL, timeout, default headers. Thêm request interceptor để attach token, response interceptor để handle errors. Test với một API thực (dùng JSONPlaceholder hoặc ReqRes).
Đây là phần quan trọng nhất. Implement logic: khi nhận 401, tự động refresh token và retry request. Phải handle concurrent requests bị 401 cùng lúc (queue mechanism). Đọc kỹ phần lý thuyết bên dưới trước khi code.
Truy cập TanStack Query docs. Đọc theo thứ tự: Overview → Quick Start → useQuery → useMutation → Query Invalidation → Infinite Queries. Chạy thử từng example.
Kết hợp Axios instance với React Query. Tạo custom hooks cho CRUD operations. Implement infinite scroll cho một list screen. Test cache invalidation khi create/update/delete.
Đây là gotcha lớn nhất. Fetch chỉ reject Promise khi có network error (mất mạng, DNS fail). Với response 404 hay 500, Fetch vẫn resolve bình thường — bạn phải tự check response.ok.
Ví dụ sai — code này KHÔNG catch được lỗi 404:
try {
const response = await fetch('https://api.example.com/users/999');
const data = await response.json();
console.log(data);
} catch (error) {
console.log('Lỗi này KHÔNG BAO GIỜ chạy cho 404');
}
Ví dụ đúng — luôn check response.ok:
const response = await fetch('https://api.example.com/users/999');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
response.json() manuallycredentials: 'include'Abort request với timeout sử dụng AbortController:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch('https://api.example.com/data', {
signal: controller.signal,
});
clearTimeout(timeoutId);
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request timed out');
}
throw error;
}
Thay vì dùng axios.get() trực tiếp, luôn tạo instance riêng. Điều này cho phép config base URL, headers, timeout một lần duy nhất và reuse khắp app.
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
const API_BASE_URL = 'https://api.example.com/v1';
const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 15000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
Request interceptor chạy TRƯỚC mỗi request được gửi đi. Dùng để attach token, add tracking headers, log requests.
apiClient.interceptors.request.use(
async (config) => {
const token = await getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
config.headers['X-Request-ID'] = generateUUID();
return config;
},
(error) => {
return Promise.reject(error);
}
);
Response interceptor xử lý response TRƯỚC khi trả về cho caller. Dùng để transform data, handle errors globally, refresh token.
apiClient.interceptors.response.use(
(response) => {
return response.data;
},
async (error) => {
if (error.response) {
switch (error.response.status) {
case 401:
break;
case 403:
break;
case 429:
break;
case 500:
break;
}
}
return Promise.reject(error);
}
);
Trong React Native, cancel request khi user rời screen để tránh memory leak và state update trên unmounted component. Từ Axios v0.22+, dùng AbortController thay cho CancelToken (đã deprecated).
const controller = new AbortController();
apiClient.get('/users', {
signal: controller.signal,
});
controller.abort();
Trong React component, cleanup khi unmount:
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const data = await apiClient.get('/users', {
signal: controller.signal,
});
setUsers(data);
} catch (error) {
if (!axios.isCancel(error)) {
setError(error);
}
}
};
fetchData();
return () => controller.abort();
}, []);
Access token có thời hạn ngắn (thường 15-30 phút) vì lý do bảo mật. Khi hết hạn, thay vì bắt user đăng nhập lại, ta dùng refresh token (thời hạn dài hơn, 7-30 ngày) để xin access token mới một cách im lặng (silent refresh).
/auth/refreshKhi access token hết hạn, có thể 3-5 requests đang chạy đồng thời và tất cả đều nhận 401. Nếu mỗi request đều gọi refresh token riêng, sẽ gây ra:
Dùng một biến flag isRefreshing và một queue chứa các requests đang chờ. Chỉ request đầu tiên nhận 401 mới thực sự gọi refresh. Các requests sau sẽ chờ trong queue và được retry sau khi refresh thành công.
Implementation đầy đủ:
import axios from 'axios';
import type {
AxiosInstance,
AxiosError,
InternalAxiosRequestConfig,
} from 'axios';
const apiClient: AxiosInstance = axios.create({
baseURL: 'https://api.example.com/v1',
timeout: 15000,
});
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: AxiosError) => void;
}> = [];
const processQueue = (
error: AxiosError | null,
token: string | null
): void => {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else {
resolve(token!);
}
});
failedQueue = [];
};
apiClient.interceptors.request.use(async (config) => {
const token = await TokenStorage.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
if (isRefreshing) {
return new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = await TokenStorage.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const { data } = await axios.post(
'https://api.example.com/v1/auth/refresh',
{ refreshToken }
);
const { accessToken, refreshToken: newRefreshToken } = data;
await TokenStorage.setTokens(accessToken, newRefreshToken);
processQueue(null, accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError as AxiosError, null);
await TokenStorage.clearTokens();
navigateToLogin();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
);
isRefreshing = false → set thành true, bắt đầu refreshisRefreshing = true → được push vào failedQueue, trả về Promise đang pendingprocessQueue resolve tất cả Promise trong queue với token mớiprocessQueue reject tất cả → redirect về LoginSử dụng react-native-keychain hoặc expo-secure-store cho production. KHÔNG dùng AsyncStorage cho tokens vì không được encrypt.
import * as SecureStore from 'expo-secure-store';
const TokenStorage = {
async getAccessToken(): Promise<string | null> {
return SecureStore.getItemAsync('access_token');
},
async getRefreshToken(): Promise<string | null> {
return SecureStore.getItemAsync('refresh_token');
},
async setTokens(
accessToken: string,
refreshToken: string
): Promise<void> {
await Promise.all([
SecureStore.setItemAsync('access_token', accessToken),
SecureStore.setItemAsync('refresh_token', refreshToken),
]);
},
async clearTokens(): Promise<void> {
await Promise.all([
SecureStore.deleteItemAsync('access_token'),
SecureStore.deleteItemAsync('refresh_token'),
]);
},
};
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
refetchOnWindowFocus: false,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<Navigation />
</QueryClientProvider>
);
}
useQuery dùng cho GET requests — đọc data. Nó tự động handle loading, error, caching, refetching.
import { useQuery } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
const useUser = (userId: number) => {
return useQuery<User>({
queryKey: ['users', userId],
queryFn: () => apiClient.get(`/users/${userId}`),
enabled: userId > 0,
staleTime: 30 * 1000,
});
};
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, isError, error, refetch } = useUser(userId);
if (isLoading) return <ActivityIndicator />;
if (isError) return <ErrorView error={error} onRetry={refetch} />;
return <Text>{data.name}</Text>;
}
Query key là identity của cache entry. React Query dùng nó để determine khi nào refetch và cache data ở đâu. Thiết kế key tốt rất quan trọng cho cache invalidation.
const queryKeys = {
users: {
all: ['users'] as const,
lists: () => [...queryKeys.users.all, 'list'] as const,
list: (filters: UserFilters) =>
[...queryKeys.users.lists(), filters] as const,
details: () => [...queryKeys.users.all, 'detail'] as const,
detail: (id: number) =>
[...queryKeys.users.details(), id] as const,
},
posts: {
all: ['posts'] as const,
byUser: (userId: number) =>
[...queryKeys.posts.all, 'user', userId] as const,
},
};
useMutation dùng cho POST/PUT/PATCH/DELETE — thay đổi data. Sau khi mutation thành công, dùng invalidateQueries hoặc setQueryData để update cache.
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreateUserInput {
name: string;
email: string;
}
const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: CreateUserInput) =>
apiClient.post('/users', input),
onSuccess: (newUser) => {
queryClient.invalidateQueries({
queryKey: queryKeys.users.lists(),
});
queryClient.setQueryData(
queryKeys.users.detail(newUser.id),
newUser
);
},
onError: (error) => {
showToast('Tạo user thất bại');
},
});
};
Update UI ngay lập tức trước khi server confirm, rollback nếu fail. Tạo UX mượt mà hơn.
const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, ...data }: UpdateUserInput) =>
apiClient.patch(`/users/${id}`, data),
onMutate: async (updatedUser) => {
await queryClient.cancelQueries({
queryKey: queryKeys.users.detail(updatedUser.id),
});
const previousUser = queryClient.getQueryData<User>(
queryKeys.users.detail(updatedUser.id)
);
queryClient.setQueryData(
queryKeys.users.detail(updatedUser.id),
(old: User | undefined) => (old ? { ...old, ...updatedUser } : old)
);
return { previousUser };
},
onError: (_error, updatedUser, context) => {
if (context?.previousUser) {
queryClient.setQueryData(
queryKeys.users.detail(updatedUser.id),
context.previousUser
);
}
},
onSettled: (_data, _error, updatedUser) => {
queryClient.invalidateQueries({
queryKey: queryKeys.users.detail(updatedUser.id),
});
},
});
};
Pattern phổ biến trong mobile: load thêm data khi user scroll tới cuối list.
import { useInfiniteQuery } from '@tanstack/react-query';
import { FlatList, ActivityIndicator } from 'react-native';
interface PaginatedResponse<T> {
data: T[];
nextCursor: string | null;
hasMore: boolean;
}
const useInfiniteUsers = () => {
return useInfiniteQuery<PaginatedResponse<User>>({
queryKey: ['users', 'infinite'],
queryFn: ({ pageParam }) =>
apiClient.get('/users', {
params: { cursor: pageParam, limit: 20 },
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) =>
lastPage.hasMore ? lastPage.nextCursor : undefined,
});
};
function UserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isError,
refetch,
} = useInfiniteUsers();
const allUsers = data?.pages.flatMap((page) => page.data) ?? [];
const handleEndReached = () => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
};
if (isLoading) return <ActivityIndicator size="large" />;
return (
<FlatList
data={allUsers}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <UserCard user={item} />}
onEndReached={handleEndReached}
onEndReachedThreshold={0.5}
refreshing={isLoading}
onRefresh={refetch}
ListFooterComponent={
isFetchingNextPage ? <ActivityIndicator /> : null
}
/>
);
}
onEndReachedThreshold={0.5} nghĩa là trigger khi còn 50% chiều cao viewport trước khi chạm đáy. Giá trị quá nhỏ → user thấy loading. Giá trị quá lớn → fetch quá sớm.hasNextPage && !isFetchingNextPage để tránh duplicate calls.onEndReached nhiều lần — React Query tự debounce nếu query đã running.interface ApiError {
status: number;
code: string;
message: string;
details?: Record<string, string[]>;
}
class ApiException extends Error {
status: number;
code: string;
details?: Record<string, string[]>;
constructor(error: ApiError) {
super(error.message);
this.name = 'ApiException';
this.status = error.status;
this.code = error.code;
this.details = error.details;
}
get isUnauthorized(): boolean {
return this.status === 401;
}
get isValidationError(): boolean {
return this.status === 422;
}
get isServerError(): boolean {
return this.status >= 500;
}
get isNetworkError(): boolean {
return this.status === 0;
}
}
apiClient.interceptors.response.use(
(response) => response.data,
(error: AxiosError<ApiError>) => {
if (!error.response) {
return Promise.reject(
new ApiException({
status: 0,
code: 'NETWORK_ERROR',
message: 'Không có kết nối mạng',
})
);
}
return Promise.reject(
new ApiException({
status: error.response.status,
code: error.response.data?.code ?? 'UNKNOWN',
message:
error.response.data?.message ?? 'Đã có lỗi xảy ra',
details: error.response.data?.details,
})
);
}
);
const retryableStatusCodes = new Set([408, 429, 500, 502, 503, 504]);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
if (failureCount >= 3) return false;
if (error instanceof ApiException) {
return retryableStatusCodes.has(error.status);
}
return false;
},
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
| Method | Mục đích | Idempotent? | Request Body? | Ví dụ |
|---|---|---|---|---|
| GET | Đọc resource | ✅ | ❌ | GET /users/123 |
| POST | Tạo resource | ❌ | ✅ | POST /users |
| PUT | Replace toàn bộ resource | ✅ | ✅ | PUT /users/123 |
| PATCH | Update một phần resource | ❌* | ✅ | PATCH /users/123 |
| DELETE | Xóa resource | ✅ | ❌ | DELETE /users/123 |
*PATCH có thể idempotent hoặc không, tùy implementation. Ví dụ {"name": "John"} là idempotent, nhưng {"$inc": {"views": 1}} thì không.
| Code | Tên | Khi nào dùng |
|---|---|---|
| 200 | OK | GET/PUT/PATCH thành công |
| 201 | Created | POST tạo resource thành công |
| 204 | No Content | DELETE thành công, không có body |
| 400 | Bad Request | Request sai format, thiếu field |
| 401 | Unauthorized | Chưa authenticate (token missing/invalid/expired) |
| 403 | Forbidden | Đã authenticate nhưng không có quyền |
| 404 | Not Found | Resource không tồn tại |
| 409 | Conflict | Duplicate resource (email đã tồn tại) |
| 422 | Unprocessable Entity | Validation errors |
| 429 | Too Many Requests | Rate limited |
| 500 | Internal Server Error | Server gặp lỗi không xác định |
| 502 | Bad Gateway | Server trung gian nhận response lỗi |
| 503 | Service Unavailable | Server tạm thời không available |
| Tiêu chí | Offset-based | Cursor-based |
|---|---|---|
| Cách dùng | ?page=2&limit=20 | ?cursor=abc123&limit=20 |
| Jump to page | ✅ Được | ❌ Không được |
| Performance lớn | ❌ Chậm (OFFSET SQL) | ✅ Nhanh (WHERE id > cursor) |
| Data thay đổi | ❌ Bị trùng/thiếu items | ✅ Ổn định |
| Phù hợp cho | Admin panels, search results | Social feeds, infinite scroll |
Mobile app chủ yếu dùng infinite scroll, không cần jump to page. Cursor-based pagination không bị ảnh hưởng khi có data mới được insert (offset sẽ bị lệch, gây duplicate items). Với dataset lớn, cursor cũng nhanh hơn nhiều vì database dùng index thay vì scan + skip.
GraphQL, WebSocket và rate limiting quan trọng nhưng không phải app nào cũng dùng. Nếu mục tiêu là build CRUD/mobile commerce thông thường, hãy học chắc Axios, token refresh, React Query, pagination và error handling trước.
GraphQL cho phép client request chính xác data cần thiết, giảm over-fetching và under-fetching.
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts(first: 5) {
title
createdAt
}
}
}
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
author {
name
}
}
}
| Tiêu chí | Apollo Client | urql |
|---|---|---|
| Bundle size | ~40KB | ~12KB |
| Cache | Normalized (mạnh, phức tạp) | Document cache (đơn giản) |
| DevTools | Rất tốt | Tốt |
| Learning curve | Cao | Thấp |
| Ecosystem | Lớn nhất | Đang phát triển |
| Khi nào dùng | App lớn, cần normalized cache | App nhỏ-trung, muốn lightweight |
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
const httpLink = createHttpLink({
uri: 'https://api.example.com/graphql',
});
const authLink = setContext(async (_, { headers }) => {
const token = await TokenStorage.getAccessToken();
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});
function App() {
return (
<ApolloProvider client={client}>
<Navigation />
</ApolloProvider>
);
}
| Tiêu chí | WebSocket | SSE (Server-Sent Events) | Long Polling |
|---|---|---|---|
| Direction | Bidirectional (full-duplex) | Server → Client only | Simulated bidirectional |
| Protocol | ws:// hoặc wss:// | HTTP | HTTP |
| Connection | Persistent | Persistent | Repeated requests |
| Binary data | ✅ | ❌ (text only) | ✅ |
| Auto reconnect | ❌ (tự implement) | ✅ (built-in) | ✅ (tự implement) |
| Overhead | Thấp sau handshake | Thấp | Cao (mỗi lần new request) |
| Use cases | Chat, gaming, real-time collab | Notifications, feeds, stock prices | Legacy systems, fallback |
| RN support | Tốt (built-in WebSocket API) | Hạn chế (cần polyfill) | Tốt (dùng fetch/axios) |
WebSocket cơ bản trong React Native:
const useWebSocket = (url: string) => {
const wsRef = useRef<WebSocket | null>(null);
const [messages, setMessages] = useState<string[]>([]);
const [isConnected, setIsConnected] = useState(false);
const reconnectAttempts = useRef(0);
const maxReconnectAttempts = 5;
const connect = useCallback(() => {
const ws = new WebSocket(url);
ws.onopen = () => {
setIsConnected(true);
reconnectAttempts.current = 0;
};
ws.onmessage = (event) => {
setMessages((prev) => [...prev, event.data]);
};
ws.onclose = () => {
setIsConnected(false);
if (reconnectAttempts.current < maxReconnectAttempts) {
const delay = Math.min(
1000 * 2 ** reconnectAttempts.current,
30000
);
reconnectAttempts.current += 1;
setTimeout(connect, delay);
}
};
ws.onerror = () => {
ws.close();
};
wsRef.current = ws;
}, [url]);
const send = useCallback((data: string) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(data);
}
}, []);
useEffect(() => {
connect();
return () => wsRef.current?.close();
}, [connect]);
return { messages, send, isConnected };
};
Khi server trả 429, response thường có header Retry-After cho biết bao lâu phải chờ (tính bằng giây). Client phải respect giá trị này.
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'];
const delayMs = retryAfter
? parseInt(retryAfter, 10) * 1000
: 5000;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return apiClient(error.config!);
}
return Promise.reject(error);
}
);
| Triệu chứng | Kiểm tra | Fix thường gặp |
|---|---|---|
| Nhiều refresh token request cùng lúc | Log 401 time, isRefreshing flag, failedQueue length | Một refresh request duy nhất, request còn lại chờ queue |
| Loading không bao giờ tắt | Timeout, abort, finally, query error state | Config timeout, handle error/finally, cancel khi unmount |
| List duplicate item khi load more | onEndReached fire nhiều lần, cursor, hasNextPage | Guard isFetchingNextPage, cursor pagination, stable key |
| Retry spam backend | Status code nào đang retry, retryDelay, jitter | Chỉ retry network/5xx/429 phù hợp, không retry 400/401/422 |
Khung trả lời phỏng vấn: API layer chuyên nghiệp gồm auth header, token refresh, typed error, timeout/cancel, retry có điều kiện, cache/invalidation và UI error matrix. Nói rõ lỗi nào retry được, lỗi nào phải để user xử lý.
Fetch API là web standard, built-in trong React Native, không cần install thêm. Tuy nhiên, Fetch không throw error cho HTTP 4xx/5xx (chỉ reject khi network error), không có interceptors, không tự transform JSON, và không có request timeout built-in (phải dùng AbortController).
Axios là third-party library. Ưu điểm: tự throw error cho non-2xx status, có interceptors (request/response), tự transform JSON, hỗ trợ timeout config, cancel requests, progress tracking cho upload.
Khi nào dùng: Fetch phù hợp cho small projects không cần interceptors. Axios phù hợp cho production apps cần token management, error handling tập trung, và response transformation. Trong thực tế, đa số production React Native apps dùng Axios kết hợp với React Query.
Flow cơ bản: Access token hết hạn → server trả 401 → client dùng refresh token xin access token mới → retry request ban đầu.
Vấn đề concurrent 401s: Nếu 5 requests cùng nhận 401, không thể gọi refresh 5 lần vì: (1) lãng phí, (2) nếu server dùng refresh token rotation, lần refresh thứ 2 sẽ fail vì token cũ đã bị invalidate.
Giải pháp — Queue mechanism:
isRefreshing flagisRefreshing = true, gọi refreshisRefreshing = true → push vào failedQueue (mảng chứa Promise resolve/reject)processQueue resolve tất cả Promises trong queue với token mới → mỗi request tự retryprocessQueue reject tất cả → redirect về loginstaleTime (default: 0): Thời gian data được coi là "fresh". Khi data còn fresh, React Query sẽ KHÔNG refetch khi component mount lại hoặc window focus. Ví dụ staleTime: 5 * 60 * 1000 — data fresh trong 5 phút.
gcTime (default: 5 phút): Thời gian data được giữ trong cache SAU KHI không còn component nào subscribe. Khi hết gcTime, data bị xóa khỏi cache (garbage collected). Ví dụ user rời screen A, data của screen A vẫn trong cache 5 phút, quay lại sẽ hiển thị ngay.
Mối quan hệ: staleTime kiểm soát khi nào refetch, gcTime kiểm soát khi nào xóa cache. Thường gcTime >= staleTime.
Offset-based (?page=2&limit=20): Dễ implement, cho phép nhảy đến page bất kỳ. Nhưng có vấn đề: khi data thay đổi (insert/delete), offset bị lệch → user thấy item trùng hoặc bỏ sót. Performance kém với dataset lớn vì database phải OFFSET (scan + skip).
Cursor-based (?cursor=abc&limit=20): Data ổn định dù có insert/delete. Performance tốt vì database dùng index (WHERE id > cursor). Nhưng không thể nhảy đến page bất kỳ.
Cho mobile: Cursor-based phù hợp hơn vì mobile chủ yếu dùng infinite scroll (không cần page numbers). Social feeds, chat histories, notification lists đều nên dùng cursor.
Optimistic update: Update UI ngay lập tức trước khi server confirm, rollback nếu server trả error. Tạo UX mượt mà, user không phải chờ loading.
Nên dùng: Các action có xác suất thành công cao và kết quả dự đoán được — like/unlike, toggle bookmark, update profile name, reorder list.
Không nên dùng: Các action có side effects phức tạp hoặc xác suất fail cao — payment, gửi email, delete account, actions cần server validation phức tạp.
Implementation: Trong React Query, dùng onMutate để snapshot data cũ + update cache, onError để rollback từ snapshot, onSettled để invalidate queries (refetch data thật từ server).
WebSocket: Full-duplex, bidirectional qua persistent connection. Client và server đều có thể gửi data bất kỳ lúc nào. Dùng cho: chat, gaming, real-time collaboration. Cần tự implement reconnection logic.
SSE (Server-Sent Events): Unidirectional (server → client), dùng HTTP standard. Có auto-reconnect built-in. Dùng cho: notifications, live feeds, stock prices. Hạn chế trong React Native (cần polyfill).
Long Polling: Client gửi request, server giữ connection open đến khi có data mới hoặc timeout. Client nhận response → gửi request mới ngay. Overhead cao nhất vì mỗi lần tạo HTTP connection mới. Dùng cho: legacy systems, fallback khi WebSocket/SSE không khả dụng.
Cho React Native: WebSocket là lựa chọn phổ biến nhất vì React Native có built-in WebSocket API và hỗ trợ tốt. Hoặc dùng Socket.IO cho tiện — nó tự fallback giữa WebSocket và polling.
Sử dụng layered error handling strategy:
Layer 1 — Axios Interceptor: Transform raw errors thành typed ApiException class. Tất cả API errors đều có structure thống nhất: status, code, message, details.
Layer 2 — React Query Global Error Handler: Config onError callback trong QueryClient để xử lý errors chung (ví dụ: show toast cho network error, redirect login cho 401).
Layer 3 — Component Level: Handle errors cụ thể cho từng screen. Ví dụ: validation error (422) hiển thị inline error messages, not found (404) hiển thị empty state.
Layer 4 — Error Boundary: Bắt unexpected errors, hiển thị fallback UI, log error lên crash reporting service (Sentry, Crashlytics).
invalidateQueries: Mark queries matching key pattern là stale. Nếu có component đang subscribe → refetch ngay. Nếu không → refetch khi component mount lại. Dùng khi bạn KHÔNG biết chính xác data mới look like gì (ví dụ: create user xong, invalidate user list — không biết server sort/filter thế nào).
setQueryData: Update cache trực tiếp, KHÔNG trigger refetch. Dùng khi biết chính xác data mới (ví dụ: API trả về updated user object → set vào cache). Nhanh hơn vì không cần network request.
Kết hợp: Thường dùng cả hai — setQueryData cho detail queries (response API trả về đầy đủ), invalidateQueries cho list queries (cần server re-sort, re-paginate).
PUT replace toàn bộ resource. Client phải gửi full object. Nếu thiếu field, field đó sẽ bị xóa hoặc set default. PUT luôn idempotent — gọi 10 lần kết quả giống gọi 1 lần.
PATCH update một phần resource. Client chỉ gửi fields muốn thay đổi. Fields không gửi không bị ảnh hưởng. PATCH có thể idempotent hoặc không, tùy implementation.
Ví dụ: User có fields {name, email, avatar}.
PUT /users/1 body {name: "John"} → email và avatar bị mấtPATCH /users/1 body {name: "John"} → chỉ name thay đổi, email và avatar giữ nguyênThực tế: Đa số REST APIs dùng PATCH cho updates vì hiếm khi muốn replace toàn bộ resource. Mobile apps gần như luôn dùng PATCH.
Exponential backoff: Tăng thời gian chờ giữa các lần retry theo cấp số nhân. Retry 1: 1s, Retry 2: 2s, Retry 3: 4s, Retry 4: 8s... Có cap tối đa (thường 30s). Tránh làm quá tải server khi server đang gặp vấn đề.
Jitter: Thêm random vào delay để tránh thundering herd — nhiều clients retry cùng lúc. Ví dụ: delay = baseDelay * 2^attempt + random(0, 1000).
Retry chỉ cho idempotent operations hoặc specific error codes:
Trong React Query: Config retry (số lần) và retryDelay (hàm tính delay). Có thể dùng function cho retry để chỉ retry với certain error codes.
Xây dựng một data-fetching layer hoàn chỉnh cho app quản lý users. Sử dụng Axios + React Query. API backend: reqres.in (free fake API).
response.data), handle errorsUser, PaginatedResponse<T>, ApiError, CreateUserInput, UpdateUserInputApiException class với computed properties (isUnauthorized, isServerError...)useUsers(page: number) — GET /users?page=XuseUser(id: number) — GET /users/:iduseCreateUser() — POST /users, invalidate list sau khi tạouseUpdateUser() — PATCH /users/:id, optimistic updateuseDeleteUser() — DELETE /users/:id, invalidate listuseInfiniteUsers() — Infinite scrollGiả lập 5 request cùng nhận 401. Chứng minh chỉ có 1 request refresh token chạy, 5 request ban đầu được retry đúng token mới.
Tạo màn hình list users với 4 trạng thái lỗi: mất mạng, 401, 422 validation, 500. Mỗi lỗi phải hiển thị UI/action khác nhau.
Dùng cursor pagination + FlatList. Test trường hợp user scroll nhanh, onEndReached fire nhiều lần, và API trả page rỗng.
Case: Sau khi access token hết hạn, Home, Notification và Profile cùng gọi API. Cả ba request nhận 401, cùng refresh token, request cuối ghi token mới nhưng request đầu retry bằng token đã bị revoke.
Cách xử lý: Dùng isRefreshing + queue. Refresh fail thì reject toàn bộ queue và điều hướng về Login một lần duy nhất. Thêm test giả lập concurrent 401 để tránh regression.
API layer tốt không chỉ “gọi được API”. Nó phải xử lý timeout, cancel, token refresh, typed error, retry có điều kiện, cache và UI state nhất quán.
setQueryData, khi nào dùng invalidateQueries?