Phân tích
6 phút đọc15 tháng 6, 2026301

React Hooks Deep Dive: Những thứ bạn nghĩ mình đã biết

Từ useState đến useTransition đào sâu vào từng hook, tránh các anti-pattern kinh điển và hiểu khi nào nên dùng cái gì.

N

Nguyễn Nhật Long

@nguyennhatlong1303

React Hooks Deep Dive: Những thứ bạn nghĩ mình đã biết

Mình đã từng nghĩ mình hiểu hooks sau khi đọc docs React xong. Rồi production bug xuất hiện stale closure trong useEffect, infinite re-render vì object dependency, context khiến cả cây component re-render không cần thiết. Thực ra hooks không khó, nhưng có khá nhiều chỗ nếu không để ý thì sẽ bị dính.

Bài này mình sẽ đi qua từng hook một cách thực tế không phải kiểu liệt kê API docs, mà là những gì mình thực sự gặp trong dự án.

useState cái bẫy nằm ở functional update

Mọi người đều biết useState, nhưng không phải ai cũng dùng đúng trong mọi trường hợp.

Với primitive value thì đơn giản:

JSX
1const [count, setCount] = useState(0)

Nhưng khi state là object, một lỗi rất hay gặp là mutate trực tiếp:

JSX
1// ❌ Sai React không detect được thay đổi
2state.name = 'new'
3setState(state)
4
5// ✅ Đúng spread ra object mới
6setState(prev => ({ ...prev, name: 'new' }))

Và đây là chỗ mình thấy nhiều bạn bỏ qua nhất functional update. Khi bạn có nhiều state update xảy ra gần nhau, hoặc update bên trong async callback, dùng setCount(count + 1) có thể cho kết quả sai vì count lúc đó là stale value. Dùng setCount(prev => prev + 1) thì React đảm bảo prev luôn là giá trị mới nhất.

Một trick nữa là lazy initialization nếu initial state cần tính toán nặng, đừng gọi hàm trực tiếp:

JSX
1// ❌ expensiveComputation() chạy mỗi lần render
2const [data, setData] = useState(expensiveComputation())
3
4// ✅ Chỉ chạy lần đầu
5const [data, setData] = useState(() => expensiveComputation())

Khác biệt nhỏ nhưng ảnh hưởng performance khá lớn nếu hàm đó tốn thời gian.

useEffect và dependency array nguồn gốc của 80% bug React

Nếu có một hook mà mình thấy bị dùng sai nhiều nhất, đó là useEffect. Không phải vì nó phức tạp, mà vì mental model của nó khác với cách chúng ta nghĩ về lifecycle.

JSX
1// Mount only
2useEffect(() => {
3 fetchData()
4}, [])
5
6// Watch deps
7useEffect(() => {
8 document.title = `Count: ${count}`
9}, [count])
10
11// Cleanup
12useEffect(() => {
13 const id = setInterval(tick, 1000)
14 return () => clearInterval(id) // ← quan trọng
15}, [])

Hai lỗi phổ biến nhất:

1. Infinite loop vì object/array trong deps

JSX
1// ❌ options là object mới mỗi render → loop vô tận
2useEffect(() => {
3 fetchData(options)
4}, [options])
5
6// ✅ Dùng useMemo hoặc move object ra ngoài component
7const options = useMemo(() => ({ page, limit }), [page, limit])

2. Dùng useEffect như event handler

JSX
1// ❌ Anti-pattern không cần useEffect cho việc này
2const [submitted, setSubmitted] = useState(false)
3useEffect(() => {
4 if (submitted) sendForm(data)
5}, [submitted])
6
7// ✅ Xử lý thẳng trong handler
8const handleSubmit = () => {
9 sendForm(data)
10}

React docs gần đây cũng nhấn mạnh điều này useEffect là để sync với external system (API, DOM, subscription), không phải để react theo user action.

React hooks data flow diagram showing useState triggering re-render, useEffect syncing with external systems like API and DOM, arrows connecting component state to side effects, clean minimal style with blue and purple accents on dark background

useRef mutable container không ai nói cho bạn nghe

useRef không chỉ để grab DOM element. Đây là cách tạo một container mutable tồn tại suốt lifecycle của component mà không trigger re-render khi thay đổi.

JSX
1// DOM ref
2const inputRef = useRef(null)
3<input ref={inputRef} />
4inputRef.current.focus()
5
6// Lưu timer ID không cần re-render khi clear
7const timerRef = useRef(null)
8useEffect(() => {
9 timerRef.current = setInterval(tick, 1000)
10 return () => clearInterval(timerRef.current)
11}, [])
12
13// Track previous value
14const prevCount = useRef(count)
15useEffect(() => {
16 prevCount.current = count
17})

Mình hay dùng ref để lưu previous value khi cần so sánh giữa render trước và render hiện tại cái này docs ít nhắc đến nhưng thực tế dùng khá nhiều.

Một pattern nâng cao hơn là callback ref thay vì truyền useRef() object, bạn truyền một function:

JSX
1const callbackRef = useCallback(node => {
2 if (node) {
3 // node vừa mount vào DOM
4 node.focus()
5 }
6}, [])
7
8<input ref={callbackRef} />

Dùng khi bạn cần biết chính xác lúc element xuất hiện hoặc biến mất khỏi DOM regular ref không cho bạn biết điều này.

useContext đừng lạm dụng

Context giải quyết prop drilling rất tốt, nhưng cái giá phải trả là performance nếu không cẩn thận.

JSX
1const ThemeContext = createContext(null)
2
3function App() {
4 const [theme, setTheme] = useState('dark')
5 return (
6 <ThemeContext.Provider value={{ theme, setTheme }}>
7 <Tree />
8 </ThemeContext.Provider>
9 )
10}
11
12function Button() {
13 const { theme } = useContext(ThemeContext)
14 return <button className={theme}>Click</button>
15}

Vấn đề: mọi consumer đều re-render khi context value thay đổi, kể cả component chỉ dùng theme nhưng setTheme thay đổi reference.

Cách fix:

Theo kinh nghiệm của mình, context phù hợp cho những thứ ít thay đổi như theme, locale, user info. Còn shopping cart, filter state, hay bất cứ thứ gì update thường xuyên thì nên dùng state management library.

Vấn đềGiải pháp
Context value thay đổi reference mỗi render`useMemo` cho value object
Một context chứa quá nhiều thứSplit thành nhiều context nhỏ
State phức tạp, nhiều component cần accessChuyển sang Zustand/Jotai

useReducer khi useState bắt đầu trở nên lộn xộn

Dấu hiệu bạn cần useReducer: bạn có 3-4 state liên quan nhau và logic update bắt đầu lặp lại ở nhiều chỗ.

JSX
1const initialState = { items: [], loading: false, error: null }
2
3function reducer(state, action) {
4 switch (action.type) {
5 case 'FETCH_START':
6 return { ...state, loading: true, error: null }
7 case 'FETCH_SUCCESS':
8 return { ...state, loading: false, items: action.payload }
9 case 'FETCH_ERROR':
10 return { ...state, loading: false, error: action.payload }
11 default:
12 return state
13 }
14}
15
16const [state, dispatch] = useReducer(reducer, initialState)
17
18// Dùng
19dispatch({ type: 'FETCH_START' })

Lợi ích rõ nhất là reducer function thuần túy, dễ test độc lập mà không cần render component. Bạn chỉ cần test reducer(state, action) trả về đúng state mới là xong.

So sánh nhanh:

useStateuseReducer
Phù hợp khiState đơn giản, ít liên quanState phức tạp, nhiều sub-values
Logic updateInline trong componentTập trung trong reducer
TestabilityPhải test qua componentTest function thuần túy
BoilerplateÍtNhiều hơn

React 19 hooks những thứ đáng để thử ngay

useId

Cái này nhỏ nhưng giải quyết một vấn đề SSR thực tế generate ID unique mà consistent giữa server và client:

JSX
1function FormField({ label }) {
2 const id = useId()
3 return (
4 <>
5 <label htmlFor={id}>{label}</label>
6 <input id={id} />
7 </>
8 )
9}

Trước đây mình hay dùng Math.random() hoặc counter tự tạo, nhưng cả hai đều có thể gây hydration mismatch. useId fix hẳn vấn đề này.

useTransition

Dùng để mark một state update là non-urgent React sẽ ưu tiên render những thứ quan trọng hơn trước:

JSX
1const [isPending, startTransition] = useTransition()
2
3const handleSearch = (query) => {
4 setInputValue(query) // urgent update input ngay
5 startTransition(() => {
6 setSearchResults(filterData(query)) // non-urgent có thể defer
7 })
8}
9
10return (
11 <>
12 <input value={inputValue} onChange={e => handleSearch(e.target.value)} />
13 {isPending ? <Spinner /> : <Results data={searchResults} />}
14 </>
15)

Mình thấy cái này hay ở chỗ: bạn không cần debounce thủ công nữa cho nhiều use case. React tự biết ưu tiên cái gì.

useDeferredValue

Tương tự useTransition nhưng dùng khi bạn không control được chỗ state update xảy ra:

JSX
1const deferredQuery = useDeferredValue(query)
2
3// ExpensiveList chỉ re-render khi React rảnh
4<ExpensiveList filter={deferredQuery} />

Khác biệt giữa hai cái:

---

useTransitionuseDeferredValue
ControlWrap update trong startTransitionWrap value nhận vào
Dùng khiBạn own code update stateState đến từ prop hoặc library khác
isPendingKhông

Anh em lưu ý một điều không có hook nào là silver bullet. useEffect không phải chỗ để xử lý mọi side effect, useContext không phải thay thế cho state management, và useReducer không phải lúc nào cũng tốt hơn useState. Hiểu đúng mental model của từng cái mới là quan trọng nhất.

Bài tiếp theo trong series mình sẽ đi vào custom hooks cách extract logic tái sử dụng và một số pattern mình thấy work tốt trong thực tế.

NN

Nguyễn Nhật Long

@nguyennhatlong1303

Nguyễn Nhật Long is a Senior Frontend Engineer and Frontend Team Leader with 7 years of experience building real-time fintech platforms. Specializing in React, Next.js, TypeScript, and React Native, shipping 10+ products across Web, Mobile, Telegram Mini-Apps, and Web3.

Thấy hay? Chia sẻ cho bạn bè!

React Hooks Deep Dive: Những thứ bạn nghĩ mình đã biết — Stacklog