Hooks Cơ bản
useState
Hook để quản lý state trong functional component.
import { useState } from 'react';
// State đơn giản
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
// State là object
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0,
});
// Cập nhật một field - phải spread toàn bộ object
function handleChange(field, value) {
setUser((prev) => ({ ...prev, [field]: value }));
}
return (
<form>
<input
value={user.name}
onChange={(e) => handleChange('name', e.target.value)}
/>
<input
value={user.email}
onChange={(e) => handleChange('email', e.target.value)}
/>
</form>
);
}
// Functional update - khi state mới phụ thuộc vào state cũ
function SafeCounter() {
const [count, setCount] = useState(0);
// ✅ Dùng functional update để tránh stale state
function increment() {
setCount((prev) => prev + 1);
}
// ❌ Có thể bị stale trong async context
function badIncrement() {
setCount(count + 1);
}
return <button onClick={increment}>Count: {count}</button>;
}
// Lazy initial state - tránh tính toán nặng mỗi lần render
function ExpensiveComponent() {
// ✅ Hàm chỉ chạy một lần khi khởi tạo
const [data, setData] = useState(() => computeExpensiveValue());
// ❌ Tính toán mỗi lần render
const [data2, setData2] = useState(computeExpensiveValue());
return <div>{data}</div>;
}
useEffect
Hook để thực hiện side effects (fetch data, subscribe, timers, v.v.)
import { useState, useEffect } from 'react';
// Chạy sau mỗi lần render
useEffect(() => {
console.log('Component rendered');
});
// Chạy một lần sau khi mount
useEffect(() => {
console.log('Component mounted');
}, []);
// Chạy khi dependency thay đổi
useEffect(() => {
console.log('userId changed:', userId);
}, [userId]);
// Cleanup function
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date());
}, 1000);
// Cleanup: chạy trước khi effect chạy lại hoặc khi unmount
return () => {
clearInterval(timer);
};
}, []);
// Fetch data pattern
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true; // Tránh update state sau khi unmount
async function fetchUser() {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
if (isMounted) {
setUser(data);
}
} catch (err) {
if (isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
}
fetchUser();
return () => {
isMounted = false;
};
}, [userId]);
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
return <div>{user?.name}</div>;
}
// Event listener
useEffect(() => {
function handleResize() {
setWindowWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// WebSocket subscription
useEffect(() => {
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (event) => {
setMessages((prev) => [...prev, event.data]);
};
return () => ws.close();
}, []);
Dependency Array - Những lỗi thường gặp
// ❌ Missing dependency
function BadComponent({ value }) {
useEffect(() => {
console.log(value); // value không ở trong dependency array
}, []);
}
// ✅ Đúng
function GoodComponent({ value }) {
useEffect(() => {
console.log(value);
}, [value]);
}
// Object/Array dependency - gây infinite loop
// ❌ Object mới mỗi lần render
function BadListComponent() {
const options = { page: 1, size: 10 }; // Mới mỗi render
useEffect(() => {
fetchData(options);
}, [options]); // Chạy mỗi render!
}
// ✅ Dùng primitive values
function GoodListComponent() {
const page = 1;
const size = 10;
useEffect(() => {
fetchData({ page, size });
}, [page, size]);
}
useRef
Hook để lưu giá trị có thể thay đổi mà không gây re-render, hoặc truy cập DOM elements.
import { useRef, useEffect } from 'react';
// 1. Truy cập DOM element
function FocusInput() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current?.focus();
}
return (
<>
<input ref={inputRef} placeholder="Click button to focus" />
<button onClick={handleClick}>Focus Input</button>
</>
);
}
// 2. Lưu giá trị không gây re-render
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function startTimer() {
intervalRef.current = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
}
function stopTimer() {
clearInterval(intervalRef.current);
}
useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
return (
<div>
<p>{seconds}s</p>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
// 3. Lưu previous value
function usePrevious(value) {
const prevRef = useRef(undefined);
useEffect(() => {
prevRef.current = value;
});
return prevRef.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}, Previous: {prevCount}</p>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
);
}
// 4. forwardRef - truyền ref xuống child component
const TextInput = forwardRef(function TextInput({ label, ...props }, ref) {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
);
});
// Sử dụng
function App() {
const inputRef = useRef(null);
return (
<>
<TextInput ref={inputRef} label="Name" />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}
useId
Hook để generate unique IDs, hữu ích cho accessibility và server-side rendering.
import { useId } from 'react';
function FormField({ label, type = 'text' }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type={type} />
</div>
);
}
// Multiple IDs từ một component
function PasswordField({ label }) {
const id = useId();
const descriptionId = `${id}-description`;
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
type="password"
aria-describedby={descriptionId}
/>
<p id={descriptionId}>Password must be at least 8 characters</p>
</div>
);
}
So sánh useState vs useRef
useState | useRef | |
|---|---|---|
| Gây re-render | ✅ Có | ❌ Không |
| Lưu qua renders | ✅ Có | ✅ Có |
| Tương tự | State | Instance variable |
| Dùng khi | Cần UI cập nhật | Không cần UI cập nhật |
| Ví dụ | Form values | Timer ID, DOM refs |