diff --git a/Task.md b/Task.md
index 8bfc609..8d55b27 100644
--- a/Task.md
+++ b/Task.md
@@ -1,31 +1,1494 @@
-- Goal:
-Add new localization (i18n) to my website using the Internationalization plugin of Umi Max so that users can switch between Vietnamese and English.
+# React Query (TanStack Query) + Axios cho React Native với Expo
-- Tasks for you:
+## Mục Lục
-1. Create full base setup for i18n in Umi Max, including:
+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)
- + Required configuration in config/config.ts
+---
- + Folder structure for locales
+## React Query là gì?
- + Base translation files (vi-VN.ts, en-US.ts)
+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:
- + Any needed runtime or app-level configuration
+- **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)
-2. Provide sample code for:
+### Tại sao nên dùng React Query?
- + Using i18n inside components (useIntl, , etc.)
+```javascript
+// ❌ Không dùng React Query
+function UserProfile() {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
- + Creating a language switcher component
+ useEffect(() => {
+ fetchUser()
+ .then(setUser)
+ .catch(setError)
+ .finally(() => setLoading(false));
+ }, []);
-3. Write a clear usage guide explaining:
+ if (loading) return ;
+ if (error) return Error: {error.message};
+ return {user.name};
+}
- + Where to place all files
+// ✅ Dùng React Query
+function UserProfile() {
+ const { data: user, isLoading, error } = useQuery({
+ queryKey: ['user'],
+ queryFn: fetchUser
+ });
- + How to import and use translations
+ if (isLoading) return ;
+ if (error) return Error: {error.message};
+ return {user.name};
+}
+```
- + How to switch languages at runtime
+---
-Make sure all examples are valid for Umi Max (Umi 4.5.0). Checked task you complete
+## 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! 🚀
\ No newline at end of file