useEffect
được thiết kế để phục vụ đồng bộ hóa component với thế giới bên ngoài, như network, DOM, nếu không có sự tham gia của các yếu tố bên ngoài, ví dụ như state cần thay đổi khi prop thay đổi, chúng ta không cần dùng đến useEffect. Hạn chế lỗi, code chạy nhanh hơn, dễ hiểu hơn là những ưu điểm khi có ít effect.
- Transform data
- User event
- Khởi chạy ứng dụng
- Subscrib trên những dữ liệu bên ngoài
- Fetch data
- Kết luận
Transform data
Ví dụ chúng ta cần filter trên một danh sách, chúng ta sẽ có xu hướng đưa việc transform này vào trong một effect với dependency là array
function TodoList({ todos, filter }) {
// 🔴 Tránh xa sử dụng cách này
const [visibleTodos, setVisibleTodos] = useState([])
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter)) }, [todos, filter])
}
// ✅ Đúng ra chỉ cần
const visibleTodos = getFilteredTodos(todos, filter)
// ✅ Nếu việc `getFilteredTodos` tốn khá nhiều thời gian để chạy
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
Ví dụ khác, kết hợp fullName
và lastName
thành userName
const [firstName, setFirstName] = useState('vui')
const [lastName, setLastName] = useState('laptrinh')
// 🔴 Tránh xa sử dụng cách này
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + ' ' + lastName)}, [firstName, lastName])
Tại sao phại phức tạp một cách không cần thiết như thế trong khi chúng ta chỉ cần viết
const [firstName, setFirstName] = useState('vui')
const [lastName, setLastName] = useState('laptrinh')
// ✅ Đúng ra chỉ cần
const fullName = firstName + ' ' + lastName
User event
Ví dụ chúng ta sẽ gọi api/buy
khi user click vào nút mua sản phẩm, sau đó hiển thị một thông báo đến user, mặc dù việc gắn sự kiện onClick
trên button rất hiển nhiên, nhưng có sẽ có người dùng effect
// 🔴 Tránh xa sử dụng cách này
useEffect(() => {
if (product.isInCart) {
showToast(`Đã thêm vào giỏ hàng`) }
}, [product])
function handleBuyClick() {
addToCart(product);
}
function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ✅ Đúng ra chỉ cần
function buyProduct() {
addToCart(product)
showToast(`Đã thêm vào giỏ hàng`)
}
function handleBuyClick() {
buyProduct(product);
}
function handleCheckoutClick() {
buyProduct(product);
navigateTo('/checkout');
}
Một ví dụ khác, cả hai POST
request đều có thể xem là side effect, tuy nhiên chỉ có một trường hợp nên dùng useEffect
// ✅ Trường hợp này ok
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' })
}, [])
// 🔴 Tránh xa sử dụng cách này
const [jsonToSubmit, setJsonToSubmit] = useState(null)
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register/', jsonToSubmit)
}
}, [jsonToSubmit])
Với trường hợp 2, việc phải chạy post('api/register'
hoàn toàn không phải do chúng ta phải hiển thị component, mà nó cần chạy vì user click submit. Đừng làm phức tạp vấn đề một cách không cần thiết
// ✅ Rất hiển nhiên
function handleSubmit(e) {
e.preventDefault()
post('/api/register', { ...our_form_data })
}
Khởi chạy ứng dụng
Khi ứng dụng bắt đầu chạy, chúng ta muốn một số hàm chạy đúng 1 lần đầu tiên
function App() {
// 🔴
useEffect(() => {
loadDatafromLocalStorage()
checkAuthToken()
}, [])
}
Tuy nhiên nó lại chạy 2 lần trên môi trường development đấy các bạn ạ! Bài này có giải thích nè, mặc dù trên production sẽ không xảy ra (zỡn kiểu này không vui đâu mấy bác team React ạ)
Chúng ta nên hiểu sự khác nhau giữa chạy một lần khi app khởi tạo khác với chạy một lần khi component mount. Để chạy ĐÚNG 1 LẦN khi app khởi tạo, chúng ta sửa lại như vầy
let didInit = false
function App() {
useEffect(() => {
if (!didInit) {
didInit = true
// ✅
loadDatafromLocalStorage()
checkAuthToken()
}
}, [])
}
Hoặc có thể chạy trước khi app render luôn
if (typeof window !== 'undefined') {
// ✅
loadDatafromLocalStorage()
checkAuthToken()
}
function App() { ... }
Subscrib trên những dữ liệu bên ngoài
Những dữ liệu bên ngoài một react component, như từ browser API, chúng ta vẫn thường dùng effect như ví dụ
function useOnlineStatus() {
const [isOnline, setIsOnLine] = useState(true)
// 🔴
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine)
}
updateState()
window.addEventListener('online', updateState)
window.addEventListener('offline', updateState)
return () => {
window.removeEventListener('online', updateState)
window.removeEventListener('offline', updateState)
}
}, [])
return isOnline
}
Cách viết này không hẳn sai hoàn toàn, chỉ là có một cách tối ưu hơn được React hỗ trợ trong trường hợp cần phải subscribe trên external store: useSyncExternalStore
function subscribe(callback) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}
function useOnlineStatus() {
// ✅
return useSyncExternalStore(
subscribe,
() => navigator.onLine, // lấy value từ client như thế nào
() => true, // lấy value này từ server như thế nào
)
}
Đọc thêm về useSyncExternalStore
Fetch data
Đây có thể là một trong những cách sử dụng effect phổ biến nhất hiện tại
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
// 🔴
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
}
Đoạn code này sẽ rơi vào tình huống race condition nếu như user nhập quá nhanh (giá trị query
thay đổi theo input của user), lúc này chúng ta sẽ cần đến debounce
, ngoài ra chúng ta sẽ còn phải nghĩ đến chuyện cache data đã lấy về để tối ưu, vâng vâng mây mây nhiều thứ khác phải xử lý. Đâu phải tự nhiên mà các framework như Next.js, Gatsby, Remix, Razzie đều giới thiệu những cách fetch data của riêng chúng
Mạnh dạng đề xuất sử dụng những thư viện thứ 3 để fetch
data thay vì dùng useEffect
, như useQuery, hoặc bèo nhất cũng phải dùng custom hook useFetch
Kết luận
Khi sử dụng useEffect
chúng ta phải hết sức để ý xem phần nào có thể tách ra thành custom hook, phần nào nên bỏ luôn, càng ít useEffect
code càng dễ hiểu và dễ maintain
Initializing...