# React Query (TanStack Query) + Axios cho React Native với Expo
## Mục Lục
1. [React Query là gì?](#react-query-là-gì)
2. [Tư duy Server-State vs Client-State](#tư-duy-server-state-vs-client-state)
3. [Chiến thuật Caching trong React Query](#chiến-thuật-caching-trong-react-query)
4. [Các Hooks quan trọng trong React Query](#các-hooks-quan-trọng-trong-react-query)
5. [Kết hợp React Query + Axios](#kết-hợp-react-query--axios)
6. [Ví dụ đầy đủ cho React Native + Expo](#ví dụ-đầy-đủ-cho-react-native--expo)
7. [Tối ưu Performance cho React Native](#tối-optim-performance-cho-react-native)
---
## React Query là gì?
React Query (nay là TanStack Query) là một library giúp quản lý **server-state** trong React applications. Nó giải quyết các vấn đề thường gặp khi làm việc với dữ liệu từ server:
- **Fetching data**: Lấy dữ liệu từ API
- **Caching**: Lưu trữ dữ liệu để tránh gọi API lại không cần thiết
- **Synchronizing**: Đồng bộ dữ liệu khi có thay đổi từ server
- **Updating**: Cập nhật dữ liệu sau khi mutation
- **Managing state**: Quản lý các trạng thái (loading, error, success)
### Tại sao nên dùng React Query?
```javascript
// ❌ Không dùng React Query
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser()
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (loading) return ;
if (error) return Error: {error.message};
return {user.name};
}
// ✅ Dùng React Query
function UserProfile() {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
if (isLoading) return ;
if (error) return Error: {error.message};
return {user.name};
}
```
---
## Tư duy Server-State vs Client-State
### Server-State
- **Nguồn gốc**: Đến từ server (API, database)
- **Đặc điểm**: Có thể bị thay đổi bởi user khác, cần đồng bộ
- **Ví dụ**: Danh sách sản phẩm, thông tin user, dữ liệu real-time
### Client-State
- **Nguồn gốc**: Tạo ra trong ứng dụng
- **Đặc điểm**: Chỉ tồn tại trong client, không cần đồng bộ
- **Ví dụ**: Theme dark/light, form input state, UI state
```javascript
function App() {
// ❌ Sai: Đẩy server-state vào client-state
const [products, setProducts] = useState([]);
// ✅ Đúng: Client-state cho UI, server-state cho data
const [isModalOpen, setIsModalOpen] = useState(false); // Client-state
const { data: products } = useQuery({ queryKey: ['products'], queryFn: fetchProducts }); // Server-state
}
```
---
## Chiến thuật Caching trong React Query
### Stale-While-Revalidate (SWR)
```javascript
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60, // 1 phút
cacheTime: 1000 * 60 * 5, // 5 phút
});
```
**Luồng hoạt động:**
1. **First request**: Gọi API → Lưu vào cache
2. **Second request (trong 1 phút)**: Trả data từ cache (instant) ✨
3. **After 1 minute**: Trả data từ cache + background refetch 🔄
4. **After 5 minutes**: Xóa khỏi cache 🗑️
### Các thời gian quan trọng
```javascript
{
staleTime: 0, // Luôn coi data là cũ (mỗi request đều gọi API)
staleTime: Infinity, // Luôn coi data là mới (không bao giờ refetch)
cacheTime: 1000 * 60 * 5, // Giữ cache 5 phút sau khi unused
refetchOnWindowFocus: true, // Refetch khi app focus (React Native)
refetchOnReconnect: true, // Refetch khi reconnect internet
refetchOnMount: false, // Không refetch khi component mount
}
```
---
## Các Hooks quan trọng trong React Query
### 1. useQuery
Dùng để fetch và cache data read-only.
```javascript
// Cú pháp cơ bản
const {
data,
isLoading,
error,
refetch,
isSuccess,
isError
} = useQuery({
queryKey: ['unique-key'],
queryFn: () => axios.get('/api/data'),
options: { /* options */ }
});
// Ví dụ thực tế
function UserList() {
const {
data: users = [],
isLoading,
error
} = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await axios.get('/api/users');
return response.data;
},
staleTime: 1000 * 60 * 5, // 5 phút
enabled: true, // Có enabled/disabled query
});
if (isLoading) return ;
if (error) return Lỗi: {error.message};
return (
{item.name}}
keyExtractor={item => item.id}
/>
);
}
```
**Options quan trọng:**
```javascript
{
// Dependencies - Query sẽ refetch khi dependencies thay đổi
queryKey: ['users', { page: 1, search: 'john' }],
// Conditional fetching
enabled: userId !== null,
// Retry config
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
// Select transformation
select: (data) => data.users.map(user => ({ ...user, fullName: `${user.firstName} ${user.lastName}` })),
}
```
### 2. useMutation
Dùng cho các tác vụ thay đổi data (POST, PUT, DELETE).
```javascript
// Cú pháp cơ bản
const mutation = useMutation({
mutationFn: (variables) => axios.post('/api/data', variables),
onSuccess: (data, variables, context) => { /* success */ },
onError: (error, variables, context) => { /* error */ },
onSettled: (data, error, variables, context) => { /* luôn chạy */ },
});
// Ví dụ thực tế
function CreateUserForm() {
const [name, setName] = useState('');
const createUserMutation = useMutation({
mutationFn: async (userData) => {
const response = await axios.post('/api/users', userData);
return response.data;
},
onSuccess: (newUser) => {
Alert.alert('Thành công', `Đã tạo user ${newUser.name}`);
queryClient.invalidateQueries({ queryKey: ['users'] }); // Refetch danh sách users
},
onError: (error) => {
Alert.alert('Lỗi', error.response?.data?.message || 'Không thể tạo user');
},
onMutate: async (newUser) => {
// Cancel ongoing queries
await queryClient.cancelQueries({ queryKey: ['users'] });
// Snapshot previous value
const previousUsers = queryClient.getQueryData(['users']);
// Optimistic update
queryClient.setQueryData(['users'], old => [...old, { id: 'temp', ...newUser }]);
return { previousUsers };
},
onError: (err, newUser, context) => {
// Rollback on error
queryClient.setQueryData(['users'], context.previousUsers);
},
});
const handleSubmit = () => {
createUserMutation.mutate({ name });
};
return (
);
}
```
### 3. useInfiniteQuery
Dùng cho pagination/scroll infinitive.
```javascript
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
refetch
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 1 }) => {
const response = await axios.get('/api/posts', {
params: { page: pageParam, limit: 10 }
});
return {
data: response.data,
nextPage: pageParam + 1,
hasMore: response.data.length === 10
};
},
getNextPageParam: (lastPage) =>
lastPage.hasMore ? lastPage.nextPage : undefined,
staleTime: 1000 * 60 * 2, // 2 phút
});
const posts = data?.pages.flatMap(page => page.data) || [];
return (
}
keyExtractor={item => item.id}
onEndReached={() => hasNextPage && fetchNextPage()}
onEndReachedThreshold={0.5}
ListFooterComponent={() =>
isFetchingNextPage ? : null
}
refreshing={isLoading}
onRefresh={refetch}
/>
);
}
```
### 4. useQueries
Fetch nhiều queries song song.
```javascript
function Dashboard() {
const queries = useQueries({
queries: [
{
queryKey: ['users'],
queryFn: () => axios.get('/api/users'),
staleTime: 1000 * 60 * 5,
},
{
queryKey: ['posts'],
queryFn: () => axios.get('/api/posts'),
staleTime: 1000 * 60 * 2,
},
{
queryKey: ['comments'],
queryFn: () => axios.get('/api/comments'),
staleTime: 1000 * 60 * 10,
},
]
});
const [usersQuery, postsQuery, commentsQuery] = queries;
const isLoading = queries.some(query => query.isLoading);
const isError = queries.some(query => query.isError);
if (isLoading) return ;
if (isError) return Lỗi tải dữ liệu;
return (
Users: {usersQuery.data?.data?.length || 0}
Posts: {postsQuery.data?.data?.length || 0}
Comments: {commentsQuery.data?.data?.length || 0}
);
}
```
### 5. useQueryClient
Access đến QueryClient instance để quản lý queries.
```javascript
function MyComponent() {
const queryClient = useQueryClient();
const handleRefresh = () => {
// Refetch tất cả queries
queryClient.refetchQueries();
// Refetch queries cụ thể
queryClient.refetchQueries({ queryKey: ['users'] });
// Prefetch data
queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 5
});
};
return ;
}
```
### 6. invalidateQueries
Mark queries như stale để trigger refetch.
```javascript
// Sau khi create/update/delete data
mutationOptions = {
onSuccess: () => {
// Invalidate tất cả queries bắt đầu với 'posts'
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Invalidate exact query
queryClient.invalidateQueries({
queryKey: ['posts', { page: 1 }]
});
// Invalidate với predicate
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'posts' &&
query.queryKey[1]?.status === 'published'
});
}
};
```
### 7. setQueryData / getQueryData
Direct access/manipulate cache.
```javascript
// Get data từ cache
const cachedUsers = queryClient.getQueryData(['users']);
// Set data vào cache
queryClient.setQueryData(['users'], newData);
// Update một item trong cache
queryClient.setQueryData(['users'], oldUsers =>
oldUsers.map(user =>
user.id === updatedUser.id ? updatedUser : user
)
);
```
### 8. cancelQueries
Cancel ongoing queries.
```javascript
// Cancel tất cả queries
queryClient.cancelQueries();
// Cancel queries cụ thể
queryClient.cancelQueries({ queryKey: ['posts'] });
// Cancel queries với predicate
queryClient.cancelQueries({
predicate: (query) => query.queryKey[0] === 'posts'
});
```
### 9. useIsFetching / useIsMutating
Check if any queries/mutations are running.
```javascript
function GlobalLoadingIndicator() {
const isFetching = useIsFetching();
const isMutating = useIsMutating();
if (isFetching || isMutating) {
return (
Đang tải...
);
}
return null;
}
```
---
## Kết hợp React Query + Axios
### 1. Tạo Axios Client với Interceptors
```javascript
// src/api/axiosClient.js
import axios from 'axios';
const axiosClient = axios.create({
baseURL: 'https://your-api.com/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor - Add token
axiosClient.interceptors.request.use(
(config) => {
const token = AsyncStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor - Handle errors & refresh token
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
axiosClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return axiosClient(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = await AsyncStorage.getItem('refresh_token');
const response = await axios.post('/auth/refresh', {
refreshToken
});
const { accessToken } = response.data;
await AsyncStorage.setItem('access_token', accessToken);
axiosClient.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
processQueue(null, accessToken);
return axiosClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
await AsyncStorage.multiRemove(['access_token', 'refresh_token']);
// Navigate to login
navigation.navigate('Login');
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
export default axiosClient;
```
### 2. Tạo API Service
```javascript
// src/api/userApi.js
import axiosClient from './axiosClient';
export const userApi = {
// Get all users
getUsers: (params = {}) => {
return axiosClient.get('/users', { params });
},
// Get user by ID
getUserById: (id) => {
return axiosClient.get(`/users/${id}`);
},
// Create user
createUser: (userData) => {
return axiosClient.post('/users', userData);
},
// Update user
updateUser: (id, userData) => {
return axiosClient.put(`/users/${id}`, userData);
},
// Delete user
deleteUser: (id) => {
return axiosClient.delete(`/users/${id}`);
},
// Upload avatar
uploadAvatar: (id, formData) => {
return axiosClient.post(`/users/${id}/avatar`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
},
};
```
### 3. Custom Hooks
```javascript
// src/hooks/useUser.js
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userApi } from '../api/userApi';
export const useUsers = (params = {}, options = {}) => {
return useQuery({
queryKey: ['users', params],
queryFn: () => userApi.getUsers(params),
staleTime: 1000 * 60 * 5, // 5 phút
...options,
});
};
export const useUser = (id, options = {}) => {
return useQuery({
queryKey: ['user', id],
queryFn: () => userApi.getUserById(id),
enabled: !!id, // Chỉ fetch khi có id
staleTime: 1000 * 60 * 10, // 10 phút
...options,
});
};
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userApi.createUser,
onSuccess: (newUser) => {
// Add new user to cache
queryClient.setQueryData(['users'], (old) => [...(old || []), newUser]);
// Invalidate để refetch nếu cần
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error) => {
console.error('Create user error:', error);
},
});
};
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, userData }) => userApi.updateUser(id, userData),
onMutate: async ({ id, userData }) => {
// Cancel ongoing queries
await queryClient.cancelQueries({ queryKey: ['user', id] });
// Snapshot previous value
const previousUser = queryClient.getQueryData(['user', id]);
// Optimistic update
queryClient.setQueryData(['user', id], (old) => ({ ...old, ...userData }));
return { previousUser };
},
onError: (error, variables, context) => {
// Rollback on error
queryClient.setQueryData(['user', variables.id], context.previousUser);
},
onSettled: (data, error, variables) => {
// Refetch để ensure data consistency
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
},
});
};
export const useDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userApi.deleteUser,
onSuccess: (_, deletedId) => {
// Remove from cache
queryClient.setQueryData(['users'], (old) =>
old.filter(user => user.id !== deletedId)
);
},
});
};
```
---
## Ví dụ đầy đủ cho React Native + Expo
### 1. App.js - Setup QueryClientProvider
```javascript
// App.js
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import AsyncStorage from '@react-native-async-storage/async-storage';
import LoginScreen from './screens/LoginScreen';
import HomeScreen from './screens/HomeScreen';
// Create QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 phút
cacheTime: 1000 * 60 * 5, // 5 phút
retry: 3,
refetchOnWindowFocus: false, // Disable cho React Native
},
mutations: {
retry: 1,
},
},
});
const Stack = createNativeStackNavigator();
export default function App() {
return (
);
}
```
### 2. LoginScreen.js
```javascript
// screens/LoginScreen.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button, Alert, StyleSheet } from 'react-native';
import { useMutation } from '@tanstack/react-query';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import { authApi } from '../api/authApi';
export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const navigation = useNavigation();
const loginMutation = useMutation({
mutationFn: (credentials) => authApi.login(credentials),
onSuccess: async (data) => {
// Save tokens
await AsyncStorage.multiSet([
['access_token', data.accessToken],
['refresh_token', data.refreshToken],
['user_info', JSON.stringify(data.user)],
]);
Alert.alert('Thành công', 'Đăng nhập thành công!');
navigation.replace('Home');
},
onError: (error) => {
Alert.alert('Lỗi', error.response?.data?.message || 'Đăng nhập thất bại');
},
});
const handleLogin = () => {
if (!email || !password) {
Alert.alert('Lỗi', 'Vui lòng nhập email và mật khẩu');
return;
}
loginMutation.mutate({ email, password });
};
return (
Đăng nhập
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 30,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
padding: 15,
borderRadius: 8,
marginBottom: 15,
fontSize: 16,
},
});
```
### 3. HomeScreen.js - User list with pull to refresh
```javascript
// screens/HomeScreen.js
import React, { useState } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
RefreshControl,
StyleSheet,
ActivityIndicator
} from 'react-native';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigation } from '@react-navigation/native';
import { userApi } from '../api/userApi';
import { useUsers, useDeleteUser } from '../hooks/useUser';
export default function HomeScreen() {
const navigation = useNavigation();
const [page, setPage] = useState(1);
const queryClient = useQueryClient();
const {
data: usersData,
isLoading,
error,
refetch,
isFetching,
} = useUsers({ page });
const deleteUserMutation = useDeleteUser();
const handleDeleteUser = (userId) => {
Alert.alert(
'Xác nhận',
'Bạn có chắc muốn xóa user này?',
[
{ text: 'Hủy', style: 'cancel' },
{
text: 'Xóa',
onPress: () => deleteUserMutation.mutate(userId),
style: 'destructive'
}
]
);
};
const renderUserItem = ({ item }) => (
navigation.navigate('UserDetail', { userId: item.id })}
>
{item.name}
{item.email}
handleDeleteUser(item.id)}
disabled={deleteUserMutation.isLoading}
>
Xóa
);
const handleLoadMore = () => {
if (!isLoading && !isFetching) {
setPage(prev => prev + 1);
}
};
if (isLoading && page === 1) {
return (
);
}
if (error) {
return (
Lỗi: {error.message}
refetch()}>
Thử lại
);
}
return (
item.id.toString()}
contentContainerStyle={styles.listContainer}
refreshControl={
}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.5}
ListFooterComponent={() =>
isFetching && page > 1 ? (
) : null
}
ListEmptyComponent={() => (
Không có user nào
)}
/>
navigation.navigate('CreateUser')}
>
+ Thêm User
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
listContainer: {
padding: 15,
},
userItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 15,
borderRadius: 8,
marginBottom: 10,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 1.41,
},
userInfo: {
flex: 1,
},
userName: {
fontSize: 16,
fontWeight: '600',
marginBottom: 5,
},
userEmail: {
fontSize: 14,
color: '#666',
},
deleteButton: {
backgroundColor: '#ff4757',
paddingHorizontal: 15,
paddingVertical: 8,
borderRadius: 5,
},
deleteButtonText: {
color: 'white',
fontWeight: '600',
},
errorText: {
fontSize: 16,
color: '#ff4757',
marginBottom: 20,
},
retryButton: {
backgroundColor: '#3742fa',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 5,
},
retryButtonText: {
color: 'white',
fontWeight: '600',
},
footerLoader: {
padding: 20,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 50,
},
addButton: {
position: 'absolute',
bottom: 30,
right: 30,
backgroundColor: '#3742fa',
paddingHorizontal: 20,
paddingVertical: 15,
borderRadius: 30,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
addButtonText: {
color: 'white',
fontWeight: '600',
fontSize: 16,
},
});
```
### 4. CreateUserScreen.js
```javascript
// screens/CreateUserScreen.js
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
Button,
StyleSheet,
Alert,
ScrollView
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useCreateUser } from '../hooks/useUser';
export default function CreateUserScreen() {
const navigation = useNavigation();
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
address: '',
});
const createUserMutation = useCreateUser();
const handleInputChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = () => {
// Validate
if (!formData.name.trim()) {
Alert.alert('Lỗi', 'Vui lòng nhập tên');
return;
}
if (!formData.email.trim()) {
Alert.alert('Lỗi', 'Vui lòng nhập email');
return;
}
// Simple email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
Alert.alert('Lỗi', 'Email không hợp lệ');
return;
}
createUserMutation.mutate(formData, {
onSuccess: () => {
Alert.alert('Thành công', 'Đã tạo user mới!');
navigation.goBack();
},
});
};
return (
Tạo User Mới
Tên *
handleInputChange('name', value)}
placeholder="Nhập tên"
/>
Email *
handleInputChange('email', value)}
placeholder="Nhập email"
keyboardType="email-address"
autoCapitalize="none"
/>
Số điện thoại
handleInputChange('phone', value)}
placeholder="Nhập số điện thoại"
keyboardType="phone-pad"
/>
Địa chỉ
handleInputChange('address', value)}
placeholder="Nhập địa chỉ"
multiline
numberOfLines={3}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
contentContainer: {
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 30,
},
formGroup: {
marginBottom: 20,
},
label: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 15,
fontSize: 16,
backgroundColor: 'white',
},
textArea: {
height: 80,
textAlignVertical: 'top',
},
buttonGroup: {
marginTop: 30,
gap: 15,
},
});
```
---
## Tối ưu Performance cho React Native
### 1. Cấu hình StaleTime/CacheTime hiệu quả
```javascript
// Quy tắc chung cho React Native
const queryConfig = {
// Data ít thay đổi (user profile, settings)
userProfile: { staleTime: 1000 * 60 * 30, cacheTime: 1000 * 60 * 60 }, // 30m stale, 1h cache
// Data thay đổi vừa phải (posts, comments)
posts: { staleTime: 1000 * 60 * 5, cacheTime: 1000 * 60 * 15 }, // 5m stale, 15m cache
// Data realtime (notifications, chat)
notifications: { staleTime: 0, cacheTime: 1000 * 60 * 2 }, // 0 stale, 2m cache
// Data tĩnh (categories, options)
categories: { staleTime: Infinity, cacheTime: Infinity }, // Never stale
};
// Sử dụng
useQuery({
queryKey: ['user', 'profile'],
queryFn: fetchUserProfile,
...queryConfig.userProfile,
});
```
### 2. Tránh gọi API lại khi đổi tab
```javascript
// Sử dụng keepPreviousData cho pagination
const { data, isPreviousData } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
keepPreviousData: true, // Giữ data cũ khi loading
staleTime: 1000 * 60 * 5,
});
// Focus refetching - chỉ refetch khi cần
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchOnWindowFocus: false, // Disable auto refetch
refetchInterval: 1000 * 30, // Refetch mỗi 30s cho data realtime
});
// Prefetch data khi hover vào tab (React Navigation)
useFocusEffect(
React.useCallback(() => {
queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
}, [queryClient])
);
```
### 3. Pull to Refresh & Infinite Scroll
```javascript
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
isRefetching,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextPage,
staleTime: 1000 * 60 * 2, // 2 phút
refetchOnWindowFocus: false,
});
const posts = React.useMemo(() =>
data?.pages.flatMap(page => page.posts) || [],
[data]
);
const renderItem = React.useCallback(({ item }) =>
,
[]
);
const keyExtractor = React.useCallback(item => item.id.toString(), []);
return (
}
// Infinite scroll
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
onEndReachedThreshold={0.5}
ListFooterComponent={() =>
isFetchingNextPage ? : null
}
// Performance optimizations
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={10}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
);
}
```
### 4. Memory Optimization
```javascript
// Clean up queries khi unmount
useEffect(() => {
return () => {
// Cancel ongoing queries
queryClient.cancelQueries({ queryKey: ['posts'] });
// Remove queries khỏi cache khi không cần
queryClient.removeQueries({ queryKey: ['temp-data'] });
};
}, [queryClient]);
// Limit cache size
const queryClient = new QueryClient({
defaultOptions: {
queries: {
cacheTime: 1000 * 60 * 10, // 10 phút
staleTime: 1000 * 60, // 1 phút
},
},
// Config cache size limits
queryCache: new QueryCache({
onError: (error) => {
console.error('Query error:', error);
},
}),
});
//选择性 refetch
const handleRefresh = () => {
// Chỉ refetch những queries cần thiết
queryClient.refetchQueries({
queryKey: ['critical-data'],
exact: false,
});
};
```
### 5. Advanced Patterns
```javascript
// Dependent queries - query phụ thuộc vào query khác
function UserProfile({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId,
});
const { data: posts } = useQuery({
queryKey: ['user-posts', userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Chỉ chạy khi có user
});
// ...
}
// Paginated queries với state management
function PaginatedList() {
const [page, setPage] = useState(1);
const [filters, setFilters] = useState({});
const { data, isLoading } = useQuery({
queryKey: ['items', page, filters],
queryFn: () => fetchItems(page, filters),
keepPreviousData: true,
});
const handleFilterChange = (newFilters) => {
setFilters(newFilters);
setPage(1); // Reset về trang 1
};
// ...
}
// Background updates cho real-time data
function RealtimeComponent() {
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchInterval: 5000, // Refetch mỗi 5s
refetchIntervalInBackground: false, // Không refetch khi app ở background
staleTime: 0,
});
}
```
### 6. Error Handling & Retry Strategies
```javascript
// Custom retry logic
const retryConfig = {
retry: (failureCount, error) => {
// Không retry cho 4xx errors
if (error.response?.status >= 400 && error.response?.status < 500) {
return false;
}
// Retry tối đa 3 lần cho 5xx hoặc network errors
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
};
// Global error boundary cho React Query
const queryClient = new QueryClient({
defaultOptions: {
queries: {
...retryConfig,
onError: (error) => {
// Global error handling
if (error.response?.status === 401) {
// Handle auth error
navigation.navigate('Login');
}
},
},
mutations: {
...retryConfig,
},
},
});
```
---
## Kết luận
React Query kết hợp với Axios tạo ra một giải pháp mạnh mẽ cho việc quản lý API state trong React Native:
1. **Performance**: Caching thông minh reduce unnecessary API calls
2. **User Experience**: Instant feedback với optimistic updates
3. **Developer Experience**: Code sạch, dễ maintain với separation of concerns
4. **Reliability**: Automatic retry, error handling, background updates
5. **Scalability**: Dễ scale khi ứng dụng phức tạp hơn
**Best practices:**
- Luôn đặt `staleTime` phù hợp với từng loại data
- Sử dụng `enabled` cho conditional queries
- Implement optimistic updates cho better UX
- Clean up queries khi unmount
- Use React.memo và useMemo cho performance optimization
Happy coding! 🚀