1494 lines
36 KiB
Markdown
1494 lines
36 KiB
Markdown
# 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 <ActivityIndicator />;
|
|
if (error) return <Text>Error: {error.message}</Text>;
|
|
return <Text>{user.name}</Text>;
|
|
}
|
|
|
|
// ✅ Dùng React Query
|
|
function UserProfile() {
|
|
const { data: user, isLoading, error } = useQuery({
|
|
queryKey: ['user'],
|
|
queryFn: fetchUser
|
|
});
|
|
|
|
if (isLoading) return <ActivityIndicator />;
|
|
if (error) return <Text>Error: {error.message}</Text>;
|
|
return <Text>{user.name}</Text>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 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 <ActivityIndicator />;
|
|
if (error) return <Text>Lỗi: {error.message}</Text>;
|
|
|
|
return (
|
|
<FlatList
|
|
data={users}
|
|
renderItem={({ item }) => <Text>{item.name}</Text>}
|
|
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 (
|
|
<View>
|
|
<TextInput value={name} onChangeText={setName} placeholder="Tên user" />
|
|
<Button
|
|
title={createUserMutation.isLoading ? "Đang tạo..." : "Tạo user"}
|
|
onPress={handleSubmit}
|
|
disabled={createUserMutation.isLoading}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<FlatList
|
|
data={posts}
|
|
renderItem={({ item }) => <PostItem post={item} />}
|
|
keyExtractor={item => item.id}
|
|
onEndReached={() => hasNextPage && fetchNextPage()}
|
|
onEndReachedThreshold={0.5}
|
|
ListFooterComponent={() =>
|
|
isFetchingNextPage ? <ActivityIndicator /> : 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 <ActivityIndicator />;
|
|
if (isError) return <Text>Lỗi tải dữ liệu</Text>;
|
|
|
|
return (
|
|
<View>
|
|
<Text>Users: {usersQuery.data?.data?.length || 0}</Text>
|
|
<Text>Posts: {postsQuery.data?.data?.length || 0}</Text>
|
|
<Text>Comments: {commentsQuery.data?.data?.length || 0}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 <Button title="Refresh" onPress={handleRefresh} />;
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<View style={styles.loadingBar}>
|
|
<ActivityIndicator size="small" />
|
|
<Text>Đang tải...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<QueryClientProvider client={queryClient}>
|
|
<NavigationContainer>
|
|
<Stack.Navigator>
|
|
<Stack.Screen name="Login" component={LoginScreen} />
|
|
<Stack.Screen name="Home" component={HomeScreen} />
|
|
</Stack.Navigator>
|
|
</NavigationContainer>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<View style={styles.container}>
|
|
<Text style={styles.title}>Đăng nhập</Text>
|
|
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Email"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
/>
|
|
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Mật khẩu"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
secureTextEntry
|
|
/>
|
|
|
|
<Button
|
|
title={loginMutation.isLoading ? "Đang đăng nhập..." : "Đăng nhập"}
|
|
onPress={handleLogin}
|
|
disabled={loginMutation.isLoading}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
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 }) => (
|
|
<TouchableOpacity
|
|
style={styles.userItem}
|
|
onPress={() => navigation.navigate('UserDetail', { userId: item.id })}
|
|
>
|
|
<View style={styles.userInfo}>
|
|
<Text style={styles.userName}>{item.name}</Text>
|
|
<Text style={styles.userEmail}>{item.email}</Text>
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
style={styles.deleteButton}
|
|
onPress={() => handleDeleteUser(item.id)}
|
|
disabled={deleteUserMutation.isLoading}
|
|
>
|
|
<Text style={styles.deleteButtonText}>Xóa</Text>
|
|
</TouchableOpacity>
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
const handleLoadMore = () => {
|
|
if (!isLoading && !isFetching) {
|
|
setPage(prev => prev + 1);
|
|
}
|
|
};
|
|
|
|
if (isLoading && page === 1) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<ActivityIndicator size="large" />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<Text style={styles.errorText}>Lỗi: {error.message}</Text>
|
|
<TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
|
|
<Text style={styles.retryButtonText}>Thử lại</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<FlatList
|
|
data={usersData?.data || []}
|
|
renderItem={renderUserItem}
|
|
keyExtractor={item => item.id.toString()}
|
|
contentContainerStyle={styles.listContainer}
|
|
refreshControl={
|
|
<RefreshControl refreshing={isFetching} onRefresh={refetch} />
|
|
}
|
|
onEndReached={handleLoadMore}
|
|
onEndReachedThreshold={0.5}
|
|
ListFooterComponent={() =>
|
|
isFetching && page > 1 ? (
|
|
<ActivityIndicator style={styles.footerLoader} />
|
|
) : null
|
|
}
|
|
ListEmptyComponent={() => (
|
|
<View style={styles.emptyContainer}>
|
|
<Text>Không có user nào</Text>
|
|
</View>
|
|
)}
|
|
/>
|
|
|
|
<TouchableOpacity
|
|
style={styles.addButton}
|
|
onPress={() => navigation.navigate('CreateUser')}
|
|
>
|
|
<Text style={styles.addButtonText}>+ Thêm User</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}>
|
|
<Text style={styles.title}>Tạo User Mới</Text>
|
|
|
|
<View style={styles.formGroup}>
|
|
<Text style={styles.label}>Tên *</Text>
|
|
<TextInput
|
|
style={styles.input}
|
|
value={formData.name}
|
|
onChangeText={(value) => handleInputChange('name', value)}
|
|
placeholder="Nhập tên"
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.formGroup}>
|
|
<Text style={styles.label}>Email *</Text>
|
|
<TextInput
|
|
style={styles.input}
|
|
value={formData.email}
|
|
onChangeText={(value) => handleInputChange('email', value)}
|
|
placeholder="Nhập email"
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.formGroup}>
|
|
<Text style={styles.label}>Số điện thoại</Text>
|
|
<TextInput
|
|
style={styles.input}
|
|
value={formData.phone}
|
|
onChangeText={(value) => handleInputChange('phone', value)}
|
|
placeholder="Nhập số điện thoại"
|
|
keyboardType="phone-pad"
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.formGroup}>
|
|
<Text style={styles.label}>Địa chỉ</Text>
|
|
<TextInput
|
|
style={[styles.input, styles.textArea]}
|
|
value={formData.address}
|
|
onChangeText={(value) => handleInputChange('address', value)}
|
|
placeholder="Nhập địa chỉ"
|
|
multiline
|
|
numberOfLines={3}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.buttonGroup}>
|
|
<Button
|
|
title={createUserMutation.isLoading ? "Đang tạo..." : "Tạo User"}
|
|
onPress={handleSubmit}
|
|
disabled={createUserMutation.isLoading}
|
|
/>
|
|
|
|
<Button
|
|
title="Hủy"
|
|
onPress={() => navigation.goBack()}
|
|
color="#ff4757"
|
|
/>
|
|
</View>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
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 }) =>
|
|
<PostItem post={item} />,
|
|
[]
|
|
);
|
|
|
|
const keyExtractor = React.useCallback(item => item.id.toString(), []);
|
|
|
|
return (
|
|
<FlatList
|
|
data={posts}
|
|
renderItem={renderItem}
|
|
keyExtractor={keyExtractor}
|
|
// Pull to refresh
|
|
refreshControl={
|
|
<RefreshControl refreshing={isRefetching} onRefresh={refetch} />
|
|
}
|
|
// Infinite scroll
|
|
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
|
onEndReachedThreshold={0.5}
|
|
ListFooterComponent={() =>
|
|
isFetchingNextPage ? <ActivityIndicator /> : 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! 🚀 |