1. useState
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
// ⚠️ State updates are asynchronous và batched
const handleClick = () => {
setCount(count + 1); // ❌ Stale closure nếu multiple calls
setCount(prev => prev + 1); // ✅ Functional update
};
// Object state
const [form, setForm] = useState({ name: '', email: '' });
const updateName = (name: string) => {
setForm(prev => ({ ...prev, name })); // ✅ Spread previous state
};
return <button onClick={handleClick}>Count: {count}</button>;
}
// Lazy initialization (expensive computation)
const [data, setData] = useState(() => {
return expensiveComputation(); // Chỉ chạy lần đầu
});
2. useEffect
import { useEffect, useState } from 'react';
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// Side effect: fetch data
let cancelled = false;
async function fetchUser() {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) {
setUser(data);
}
}
fetchUser();
// Cleanup function
return () => {
cancelled = true; // Prevent state update after unmount
};
}, [userId]); // Dependency array: re-run when userId changes
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
// useEffect dependency patterns:
// useEffect(() => {}, []); // Mount only (componentDidMount)
// useEffect(() => {}); // Every render (⚠️ avoid)
// useEffect(() => {}, [dep]); // When dep changes
// useEffect(() => { return () => {} }, []); // Unmount cleanup
3. useRef
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
const renderCount = useRef(0); // Mutable value, no re-render
useEffect(() => {
inputRef.current?.focus(); // Auto-focus on mount
}, []);
useEffect(() => {
renderCount.current += 1; // Track renders without causing re-render
});
return (
<div>
<input ref={inputRef} type="text" />
<p>Rendered {renderCount.current} times</p>
</div>
);
}
4. useMemo & useCallback
import { useMemo, useCallback, useState } from 'react';
function ProductList({ products, category }: Props) {
// useMemo: cache kết quả computation
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === category);
}, [products, category]); // Chỉ re-compute khi dependencies thay đổi
// useCallback: cache function reference
const handleDelete = useCallback((id: number) => {
// delete logic
}, []); // Stable reference, không tạo function mới mỗi render
return (
<div>
{filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onDelete={handleDelete} // Stable reference → prevent re-render
/>
))}
</div>
);
}
// ⚠️ Khi nào dùng useMemo/useCallback:
// ✅ Expensive computations
// ✅ Passing callbacks to memoized children (React.memo)
// ✅ Dependencies of other hooks
// ❌ KHÔNG dùng cho mọi thứ — premature optimization
5. useReducer
import { useReducer } from 'react';
interface State {
items: CartItem[];
total: number;
isLoading: boolean;
}
type Action =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: { id: number } }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'CLEAR_CART' };
function cartReducer(state: State, action: Action): State {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
case 'REMOVE_ITEM':
const item = state.items.find(i => i.id === action.payload.id);
return {
...state,
items: state.items.filter(i => i.id !== action.payload.id),
total: state.total - (item?.price || 0),
};
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'CLEAR_CART':
return { items: [], total: 0, isLoading: false };
default:
return state;
}
}
function Cart() {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
total: 0,
isLoading: false,
});
return (
<div>
<p>Total: ${state.total}</p>
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
Clear Cart
</button>
</div>
);
}
6. Custom Hooks
// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// useFetch
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setIsLoading(true);
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!cancelled) setData(json);
} catch (err) {
if (!cancelled) setError(err as Error);
} finally {
if (!cancelled) setIsLoading(false);
}
}
fetchData();
return () => { cancelled = true; };
}, [url]);
return { data, error, isLoading };
}
// Usage
function Users() {
const { data: users, error, isLoading } = useFetch<User[]>('/api/users');
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 300);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <div>{/* render users */}</div>;
}
7. React 19 Hooks
import { useTransition, useDeferredValue, use } from 'react';
// useTransition: mark state update as non-urgent
function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (value: string) => {
setQuery(value); // Urgent: update input immediately
startTransition(() => {
// Non-urgent: update filtered list (can be interrupted)
setFilteredItems(filterItems(value));
});
};
return (
<div>
<input value={query} onChange={e => handleChange(e.target.value)} />
{isPending && <Spinner />}
</div>
);
}
// useDeferredValue: defer re-rendering of a value
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<Results query={deferredQuery} />
</div>
);
}
Bài tiếp theo: Event Handling, Forms & Conditional Rendering — controlled forms, React Hook Form, và Zod validation.