Files
FE-DEVICE-SGW/Task.md
2025-12-08 16:07:04 +07:00

36 KiB

React Query (TanStack Query) + Axios cho React Native với Expo

Mục Lục

  1. React Query là gì?
  2. Tư duy Server-State vs Client-State
  3. Chiến thuật Caching trong React Query
  4. Các Hooks quan trọng trong React Query
  5. 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

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?

// ❌ 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
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)

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

{
  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.

// 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:

{
  // 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).

// 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.

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.

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.

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.

// 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.

// 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.

// 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.

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

// 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

// 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

// 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

// 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

// 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

// 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  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

// 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ả

// 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

// 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

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

// 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

// 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

// 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! 🚀