Ở giai đoạn đầu, phần lớn ứng dụng React Native có thể chạy tốt với useState và useEffect. Nhưng khi màn hình có nhiều trạng thái liên quan, danh sách bắt đầu giật, component con render lại quá nhiều hoặc logic bị sao chép ở nhiều nơi, chúng ta cần thêm công cụ.
Bài này đi sâu vào các hook nâng cao thường gặp trong dự án React Native: useReducer, useContext, useMemo, useCallback, useRef, useLayoutEffect, useImperativeHandle, useTransition, useDeferredValue, useSyncExternalStore và custom hook. Mục tiêu không phải dùng thật nhiều hook, mà là biết chính xác vấn đề nào đáng giải quyết bằng hook nào.
Điều kiện trước khi đọc: bạn nên nắm component, props, state, vòng đời render và
useEffect. Có thể xem lại Hooks trong React Native cho người mới và cách dùng useEffect đúng.
Nội dung chính
- Tư duy trước khi tối ưu bằng hook
- useReducer: quản lý state có nhiều nhánh
- useContext: chia sẻ dữ liệu theo cây component
- useMemo và useCallback: tối ưu có đo lường
- useRef: giữ dữ liệu mà không render lại
- useLayoutEffect và useImperativeHandle
- useTransition và useDeferredValue
- useSyncExternalStore
- Thiết kế custom hook dễ tái sử dụng
- Checklist chọn hook
1. Tư duy trước khi tối ưu bằng hook
Một hook nâng cao chỉ có giá trị khi nó làm code dễ hiểu hơn hoặc giải quyết một vấn đề đã quan sát được. Đừng thêm useMemo và useCallback vào mọi biến, mọi hàm theo phản xạ. Memoization cũng có chi phí: React phải giữ giá trị cũ, so sánh dependency và làm code khó đọc hơn.
Trước khi chọn hook, hãy trả lời bốn câu hỏi:
- State này có nhiều hành động và nhiều nhánh chuyển đổi không?
- Giá trị này cần tồn tại qua các lần render nhưng thay đổi của nó có cần cập nhật UI không?
- Component thực sự chậm hay chỉ đang render lại nhưng rất nhẹ?
- Logic này có đang lặp lại ở từ hai component trở lên không?
Trong React Native, render JavaScript không đồng nghĩa với việc toàn bộ native view bị dựng lại. Vì vậy, một lần render thêm chưa chắc là vấn đề. Hãy dùng React DevTools Profiler, log có chủ đích và đo trên thiết bị thật trước khi tối ưu.
2. useReducer: quản lý state có nhiều nhánh
useReducer phù hợp khi nhiều giá trị state thay đổi cùng nhau, hoặc khi màn hình có những hành động rõ ràng như bắt đầu tải, tải thành công, tải thất bại và thử lại. Thay vì rải nhiều lời gọi setState, reducer gom quy tắc chuyển đổi state vào một hàm thuần.
import React, {useEffect, useReducer} from 'react';
import {ActivityIndicator, Button, Text, View} from 'react-native';
type User = {id: number; name: string};
type State = {
status: 'idle' | 'loading' | 'success' | 'error';
users: User[];
error: string | null;
};
type Action =
| {type: 'FETCH_START'}
| {type: 'FETCH_SUCCESS'; payload: User[]}
| {type: 'FETCH_ERROR'; payload: string};
const initialState: State = {
status: 'idle',
users: [],
error: null,
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START':
return {...state, status: 'loading', error: null};
case 'FETCH_SUCCESS':
return {status: 'success', users: action.payload, error: null};
case 'FETCH_ERROR':
return {...state, status: 'error', error: action.payload};
default:
return state;
}
}
Component chỉ còn nhiệm vụ phát sự kiện bằng dispatch:
function UserScreen() {
const [state, dispatch] = useReducer(reducer, initialState);
const loadUsers = async () => {
dispatch({type: 'FETCH_START'});
try {
const response = await fetch('https://example.com/users');
if (!response.ok) throw new Error('Không thể tải người dùng');
const users: User[] = await response.json();
dispatch({type: 'FETCH_SUCCESS', payload: users});
} catch (error) {
const message =
error instanceof Error ? error.message : 'Có lỗi xảy ra';
dispatch({type: 'FETCH_ERROR', payload: message});
}
};
useEffect(() => {
loadUsers();
}, []);
if (state.status === 'loading') return <ActivityIndicator />;
if (state.status === 'error') {
return (
<View>
<Text>{state.error}</Text>
<Button title="Thử lại" onPress={loadUsers} />
</View>
);
}
return (
<View>
{state.users.map(user => (
<Text key={user.id}>{user.name}</Text>
))}
</View>
);
}
Reducer phải là hàm thuần: không gọi API, không ghi storage và không điều hướng bên trong reducer. Side effect vẫn nằm ở event handler hoặc useEffect. TypeScript với discriminated union giúp phát hiện action thiếu payload ngay khi viết code.
Khi nào chưa cần useReducer?
Nếu màn hình chỉ có một ô tìm kiếm và một biến isLoading độc lập, useState thường dễ đọc hơn. Hãy chuyển sang reducer khi các state bắt đầu phụ thuộc nhau hoặc các quy tắc chuyển đổi khó theo dõi.
3. useContext: chia sẻ dữ liệu theo cây component
useContext giúp truyền dữ liệu xuống sâu mà không phải chuyển props qua nhiều tầng. Các trường hợp phù hợp gồm theme, phiên đăng nhập, ngôn ngữ hoặc cấu hình ứng dụng.
import React, {createContext, useContext, useMemo, useState} from 'react';
type Session = {token: string; displayName: string} | null;
type AuthContextValue = {
session: Session;
signIn: (session: NonNullable<Session>) => void;
signOut: () => void;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export function AuthProvider({children}: React.PropsWithChildren) {
const [session, setSession] = useState<Session>(null);
const value = useMemo(
() => ({
session,
signIn: setSession,
signOut: () => setSession(null),
}),
[session],
);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth phải được dùng bên trong AuthProvider');
}
return context;
}
Mỗi khi value của provider đổi, các component đang đọc context có thể render lại. Với context lớn và cập nhật liên tục, hãy tách thành nhiều context theo trách nhiệm, ví dụ AuthStateContext và AuthActionsContext. Không nên biến một context duy nhất thành kho chứa mọi state của ứng dụng.
4. useMemo và useCallback: tối ưu có đo lường
useMemo ghi nhớ một giá trị
useMemo hữu ích khi phép tính tương đối nặng hoặc khi cần giữ tham chiếu ổn định cho object truyền vào component đã được memo hóa.
const visibleProducts = useMemo(() => {
const keyword = query.trim().toLowerCase();
return products
.filter(item => item.name.toLowerCase().includes(keyword))
.sort((a, b) => b.rating - a.rating);
}, [products, query]);
Không dùng useMemo cho những phép tính rất rẻ như nối hai chuỗi. React có thể bỏ cache trong một số tình huống; vì vậy, code không được phụ thuộc vào useMemo để đảm bảo tính đúng đắn.
useCallback ghi nhớ một hàm
useCallback thường có ý nghĩa khi callback được truyền vào component con dùng React.memo, hoặc callback là dependency của hook khác.
type Product = {id: string; name: string};
type RowProps = {
item: Product;
onPress: (id: string) => void;
};
const ProductRow = React.memo(function ProductRow({item, onPress}: RowProps) {
return (
<Pressable onPress={() => onPress(item.id)}>
<Text>{item.name}</Text>
</Pressable>
);
});
function ProductList({products}: {products: Product[]}) {
const navigation = useNavigation();
const openDetail = useCallback(
(id: string) => navigation.navigate('ProductDetail', {id}),
[navigation],
);
const renderItem = useCallback(
({item}: {item: Product}) => (
<ProductRow item={item} onPress={openDetail} />
),
[openDetail],
);
return (
<FlatList
data={products}
keyExtractor={item => item.id}
renderItem={renderItem}
/>
);
}
Lưu ý: chỉ bọc renderItem bằng useCallback chưa chắc làm danh sách nhanh hơn. Tối ưu thường cần kết hợp key ổn định, row component gọn, React.memo, kích thước item hợp lý và tránh tạo object style mới không cần thiết. Xem thêm bài xây danh sách với FlatList.
Ba lỗi dependency phổ biến
- Bỏ dependency để “ngăn render”, khiến callback đọc state cũ.
- Đưa object được tạo mới mỗi render vào dependency, làm cache luôn mất hiệu lực.
- Memo hóa mọi thứ mà không đo, khiến code dài hơn nhưng hiệu năng không đổi.
5. useRef: giữ dữ liệu mà không render lại
useRef lưu một object ổn định qua toàn bộ vòng đời component. Khi thay đổi ref.current, React không render lại UI. Có ba nhóm tình huống phổ biến.
5.1. Truy cập native component
function SearchBox() {
const inputRef = useRef<TextInput>(null);
return (
<View>
<TextInput ref={inputRef} placeholder="Nhập từ khóa" />
<Button
title="Focus ô tìm kiếm"
onPress={() => inputRef.current?.focus()}
/>
</View>
);
}
5.2. Lưu timer và dọn dẹp đúng cách
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
timerRef.current = setTimeout(() => {
console.log('Hoàn tất');
}, 1000);
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
5.3. Lưu giá trị trước đó
function usePrevious<T>(value: T) {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
Không dùng ref thay state chỉ để né render. Nếu một thay đổi cần hiển thị trên màn hình, nó phải nằm trong state. Ref phù hợp với dữ liệu phục vụ xử lý nội bộ như timer, request ID, giá trị trước đó hoặc handle của native component.
6. useLayoutEffect và useImperativeHandle
useLayoutEffect: đồng bộ trước khi màn hình được vẽ
useLayoutEffect chạy đồng bộ sau khi React cập nhật cây view nhưng trước khi thay đổi được hiển thị. Trong React Native, tình huống dễ gặp nhất là cập nhật header của React Navigation để tránh tiêu đề nháy từ giá trị mặc định sang giá trị mới.
function ProfileScreen({navigation, route}) {
const {user} = route.params;
useLayoutEffect(() => {
navigation.setOptions({
title: user.displayName,
});
}, [navigation, user.displayName]);
return <Profile user={user} />;
}
Vì hook này chặn việc hiển thị, không đặt tác vụ nặng hoặc gọi API bên trong nó. Phần lớn side effect vẫn nên dùng useEffect.
useImperativeHandle: công khai một API nhỏ qua ref
Đôi khi component cha cần gọi hành động mang tính mệnh lệnh như focus, clear hoặc open. useImperativeHandle cho phép chỉ công khai đúng các hành động cần thiết thay vì để cha truy cập toàn bộ component con.
type SearchInputHandle = {
focus: () => void;
clear: () => void;
};
type Props = {
value: string;
onChangeText: (value: string) => void;
};
const SearchInput = React.forwardRef<SearchInputHandle, Props>(
({value, onChangeText}, ref) => {
const inputRef = useRef<TextInput>(null);
useImperativeHandle(
ref,
() => ({
focus: () => inputRef.current?.focus(),
clear: () => onChangeText(''),
}),
[onChangeText],
);
return (
<TextInput
ref={inputRef}
value={value}
onChangeText={onChangeText}
/>
);
},
);
Đây nên là ngoại lệ, không phải cách giao tiếp mặc định giữa component. Với dữ liệu thông thường, props và callback vẫn rõ ràng hơn.
7. useTransition và useDeferredValue
Hai hook này giúp ưu tiên cập nhật quan trọng, chẳng hạn thao tác nhập liệu, trước cập nhật có thể trì hoãn như lọc một danh sách lớn. Khả năng hoạt động phụ thuộc phiên bản React đi kèm React Native, vì vậy cần kiểm tra phiên bản trong dự án.
useTransition: đánh dấu cập nhật có độ ưu tiên thấp
function ProductSearch({products}: {products: Product[]}) {
const [query, setQuery] = useState('');
const [filtered, setFiltered] = useState(products);
const [isPending, startTransition] = useTransition();
const handleChange = (text: string) => {
setQuery(text); // Cập nhật TextInput ngay
startTransition(() => {
const keyword = text.trim().toLowerCase();
setFiltered(
products.filter(item =>
item.name.toLowerCase().includes(keyword),
),
);
});
};
return (
<View>
<TextInput value={query} onChangeText={handleChange} />
{isPending && <ActivityIndicator />}
<ProductList products={filtered} />
</View>
);
}
useDeferredValue: trì hoãn giá trị truyền xuống phần UI nặng
function SearchScreen({products}: {products: Product[]}) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const filtered = useMemo(() => {
const keyword = deferredQuery.trim().toLowerCase();
return products.filter(item =>
item.name.toLowerCase().includes(keyword),
);
}, [deferredQuery, products]);
const isStale = query !== deferredQuery;
return (
<View>
<TextInput value={query} onChangeText={setQuery} />
<View style={{opacity: isStale ? 0.6 : 1}}>
<ProductList products={filtered} />
</View>
</View>
);
}
useTransition và useDeferredValue không phải debounce, không giảm số request mạng và không thay thế thư viện animation chạy trên UI thread. Nếu tìm kiếm gọi API, vẫn cần debounce hoặc hủy request cũ. Nếu animation bị giật, hãy xem xét Animated hoặc Reanimated thay vì kỳ vọng transition xử lý animation native.
8. useSyncExternalStore: đọc dữ liệu ngoài React an toàn
useSyncExternalStore dành cho dữ liệu được quản lý bên ngoài state của React và có cơ chế subscribe, ví dụ một store tự viết, trạng thái kết nối mạng hoặc SDK phát sự kiện. Hook đảm bảo React đọc snapshot nhất quán, kể cả trong cơ chế render đồng thời.
import {useSyncExternalStore} from 'react';
type Listener = () => void;
let online = true;
const listeners = new Set<Listener>();
export const networkStore = {
getSnapshot: () => online,
subscribe: (listener: Listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
setOnline: (nextValue: boolean) => {
online = nextValue;
listeners.forEach(listener => listener());
},
};
export function useOnlineStatus() {
return useSyncExternalStore(
networkStore.subscribe,
networkStore.getSnapshot,
);
}
Trong dự án thật, phần adapter có thể lắng nghe NetInfo hoặc một SDK native, sau đó cập nhật store. Nếu đang dùng Zustand, Redux hoặc một thư viện state phổ biến, hãy ưu tiên hook chính thức của thư viện; họ đã xử lý subscription và selector.
9. Custom hook: đóng gói logic, không đóng gói giao diện
Custom hook là một hàm bắt đầu bằng use và có thể gọi các hook khác. Nó giúp đặt tên cho một hành vi, tái sử dụng logic và làm component tập trung vào UI.
Ví dụ 1: useDebouncedValue
function useDebouncedValue<T>(value: T, delay = 400) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
Cách dùng với tìm kiếm API:
const [query, setQuery] = useState('');
const debouncedQuery = useDebouncedValue(query, 500);
useEffect(() => {
if (!debouncedQuery.trim()) return;
const controller = new AbortController();
searchProducts(debouncedQuery, controller.signal);
return () => controller.abort();
}, [debouncedQuery]);
Ví dụ 2: useAppState
import {AppState, AppStateStatus} from 'react-native';
function useAppState() {
const [appState, setAppState] = useState<AppStateStatus>(
AppState.currentState,
);
useEffect(() => {
const subscription = AppState.addEventListener('change', setAppState);
return () => subscription.remove();
}, []);
return appState;
}
Hook này có thể dùng để làm mới dữ liệu khi ứng dụng quay lại foreground:
const appState = useAppState();
const previousAppState = usePrevious(appState);
useEffect(() => {
const returnedToForeground =
previousAppState !== 'active' && appState === 'active';
if (returnedToForeground) {
refreshData();
}
}, [appState, previousAppState, refreshData]);
Nguyên tắc thiết kế custom hook
- Đặt tên theo hành vi:
useAuth,useAppState,useDebouncedValue. - Giữ API đầu vào và đầu ra nhỏ, rõ nghĩa.
- Không che giấu quá nhiều hành vi bất ngờ như tự điều hướng hoặc tự hiện toast.
- Luôn cleanup listener, timer và request khi component unmount.
- Trả về object khi có nhiều giá trị để code gọi hook dễ đọc.
- Test logic quan trọng độc lập bằng thư viện test hook hoặc qua component sử dụng nó.
10. Rules of Hooks và lỗi stale closure
Dù dùng hook cơ bản hay nâng cao, hai quy tắc vẫn không thay đổi: chỉ gọi hook ở top level và chỉ gọi hook bên trong function component hoặc custom hook. Không gọi hook trong if, vòng lặp hay callback thông thường.
Stale closure xảy ra khi một callback giữ lại state của lần render cũ. Ví dụ sau luôn tăng từ giá trị cũ nếu callback được lưu và gọi về sau:
// Dễ đọc phải state cũ trong callback bất đồng bộ
setTimeout(() => setCount(count + 1), 1000);
// An toàn khi cập nhật dựa trên giá trị trước đó
setTimeout(() => setCount(current => current + 1), 1000);
Đừng tắt cảnh báo dependency của ESLint chỉ để hết lỗi. Cảnh báo thường cho biết mô hình dữ liệu đang chưa rõ: có thể cần functional update, chuyển logic vào effect, đưa hàm ra ngoài component hoặc ổn định callback đúng cách.
11. Checklist chọn hook trong dự án React Native
- State đơn giản, độc lập: dùng
useState. - State có nhiều hành động, nhiều nhánh: dùng
useReducer. - Dữ liệu dùng ở nhiều tầng component: cân nhắc
useContext. - Phép tính nặng hoặc object cần tham chiếu ổn định: cân nhắc
useMemo. - Callback truyền vào component đã memo hóa: cân nhắc
useCallback. - Timer, native handle, giá trị trước đó: dùng
useRef. - Cập nhật layout/header trước khi hiển thị: dùng
useLayoutEffect. - Cha cần gọi focus/open/clear ở component con: dùng
useImperativeHandlecó giới hạn. - Giữ nhập liệu phản hồi nhanh khi UI phía dưới nặng: cân nhắc
useTransitionhoặcuseDeferredValue. - Subscribe store nằm ngoài React: dùng
useSyncExternalStorehoặc hook chính thức của thư viện. - Logic lặp lại ở nhiều nơi: tách custom hook.
Kết luận
Hook nâng cao không làm ứng dụng tốt hơn chỉ vì chúng “nâng cao”. Giá trị thật nằm ở việc chọn đúng công cụ: reducer giúp state dễ dự đoán, context giảm truyền props, memoization giải quyết điểm nghẽn đã đo được, ref lưu dữ liệu không phục vụ render, còn custom hook tạo ra ngôn ngữ riêng cho nghiệp vụ của ứng dụng.
Một lộ trình luyện tập hợp lý là lấy màn hình gọi API đã có, chuyển trạng thái tải sang useReducer, tách logic thành custom hook, sau đó dùng Profiler kiểm tra danh sách trước và sau khi áp dụng React.memo, useMemo hoặc useCallback. Đây là cách biến kiến thức hook thành kỹ năng thiết kế component thực tế.
Đọc tiếp: Gọi API trong React Native: loading, error và empty state.




