Một số trường hợp không nên sử dụng hook useEffect:
Updating State Based on Props or State
Component dưới đây giúp tự động tính toán lại giá trị của fullName
khi firstName
và lastName
có sự thay đổi:
import { useState, useEffect } from 'react'
function Form() {
const [firstName, setFirstName] = useState('Taylor')
const [lastName, setLastName] = useState('Swift')
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + ' ' + lastName)
}, [firstName, lastName])
// ...
}
Ta có thể không cần dùng useEffect
và sử dụng fullName
như là state bởi vì nó có thể được tính toán trực tiếp trong quá trình render như sau:
import { useState } from 'react'
function Form() {
const [firstName, setFirstName] = useState('Taylor')
const [lastName, setLastName] = useState('Swift')
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName
// ...
}
Seealso
Xem thêm State để biết khi nào thì cần dùng state.
Caching Expensive Calculations
Component sau đây lưu mảng đã được filter (visibleTodos
) ở trong một state và thực hiện filter lại bất cứ khi nào props (todos
và filter
) bị thay đổi.
import { useState, useEffect } from 'react'
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('')
// 🔴 Avoid: redundant state and unnecessary Effect
const [visibleTodos, setVisibleTodos] = useState([])
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter))
}, [todos, filter])
// ...
}
Chúng ta có thể filter mảng trong khi đang render mà không cần sử dụng useState và useEffect như sau:
import { useState } from 'react'
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('')
// ✅ This is fine if getFilteredTodos() is not slow.
const visibleTodos = getFilteredTodos(todos, filter)
// ...
}
Trong trường hợp mảng todos
là quá lớn và cần thời gian xử lý hơn 1ms
, ta có thể dùng hook useMemo như sau:
import { useMemo, useState } from 'react'
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('')
const visibleTodos = useMemo(() => {
// ✅ Does not re-run unless todos or filter change
return getFilteredTodos(todos, filter)
}, [todos, filter])
// ...
}
Giá trị của visibleTodos
sẽ được tính toán lần đầu khi component vừa được mount vào giao diện. Chỉ khi nào giá trị của todos
hay filter
thay đổi giữa những lần re-render thì visibleTodos
mới được tính toán lại. Nói cách khác, useMemo
giúp chúng ta bỏ qua được những lần tính toán giá trị không cần thiết.
Callback truyền vào useMemo
sẽ được chạy trong quá trình render nên nó cần phải là một pure function1.
Sharing Logic Between Event Handlers
Giả sử ta có một trang sản phẩm với hai nút (“Buy” và “Checkout”) đều cho phép người dùng mua một sản phẩm. Chúng ta muốn hiển thị thông báo bất cứ khi nào người dùng cho sản phẩm vào giỏ hàng. Do việc hiển thị thông báo mang tính chất lặp lại nên ta có thể có ý định dùng useEffect
dựa trên sự thay đổi của product
:
import { useEffect } from 'react'
function ProductPage({ product, addToCart }) {
// 🔴 Avoid: Event-specific logic inside an Effect
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`)
}
}, [product])
function handleBuyClick() {
addToCart(product)
}
function handleCheckoutClick() {
addToCart(product)
navigateTo('/checkout')
}
// ...
}
Giả sử ứng dụng của chúng ta có thể nhớ được sản phẩm nào đã được thêm vào giỏ hàng giữa các lần refresh. Khi người dùng thêm vào giỏ hàng một sản phẩm và refresh lại trang, dòng thông báo sẽ xuất hiện lại một lần nữa. Lý do là vì setup function của useEffect
không chỉ được gọi khi dependency bị thay đổi mà nó còn được gọi sau khi component được mount.
Để giải quyết vấn đề này, ta có thể viết một hàm rồi gọi sử dụng ở hai event handler như sau:
function ProductPage({ product, addToCart }) {
// ✅ Good: Event-specific logic is called from event handlers
function buyProduct() {
addToCart(product)
showNotification(`Added ${product.name} to the shopping cart!`)
}
function handleBuyClick() {
buyProduct()
}
function handleCheckoutClick() {
buyProduct()
navigateTo('/checkout')
}
// ...
}
Important
Hook
useEffect
là để xử lý những effect sau khi component được hiển thị chứ không phải là để phản hồi với các sự kiện. Trong ví dụ trên, dòng thông báo xuất hiện bởi vì người dùng thêm sản phẩm vào giỏ hàng chứ không phải bởi vì người dùng thấy component ở trên màn hình.
Chains of Computations
Đôi khi chúng ta dùng useEffect
để cập nhật state dựa trên sự thay đổi của state khác nhiều lần liên tiếp như sau:
function Game() {
const [card, setCard] = useState(null)
const [goldCardCount, setGoldCardCount] = useState(0)
const [round, setRound] = useState(1)
const [isGameOver, setIsGameOver] = useState(false)
// 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount((c) => c + 1)
}
}, [card])
useEffect(() => {
if (goldCardCount > 3) {
setRound((r) => r + 1)
setGoldCardCount(0)
}
}, [goldCardCount])
useEffect(() => {
if (round > 5) {
setIsGameOver(true)
}
}, [round])
useEffect(() => {
alert('Good game!')
}, [isGameOver])
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.')
} else {
setCard(nextCard)
}
}
// ...
}
Khi có sự kiện xảy ra, đoạn code trên sẽ re-render rất nhiều lần, cụ thể:
setCard ➡️ render ➡️ setGoldCardCount ➡️ render ➡️ setRound ➡️ render ➡️ setIsGameOver ➡️ render
Để giải quyết vấn đề trên, ta có thể tính toán các giá trị trong quá trình render và trong event handler thay vì sử dụng useEffect
:
function Game() {
const [card, setCard] = useState(null)
const [goldCardCount, setGoldCardCount] = useState(0)
const [round, setRound] = useState(1)
// ✅ Calculate what you can during rendering
const isGameOver = round > 5
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.')
}
// ✅ Calculate all the next state in the event handler
setCard(nextCard)
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1)
} else {
setGoldCardCount(0)
setRound(round + 1)
if (round === 5) {
alert('Good game!')
}
}
}
}
// ...
}
Trong trường hợp không thể tính toán giá trị của state mới ở trong event handler, chẳng hạn tính toán các lựa chọn có thể có của một dropdown phụ thuộc vào lựa chọn của dropdown trước đó, ta có thể dùng useEffect
.
Notifying Parent Components About State Changes
Giả sử ta muốn thông báo cho parent component rằng state của child component đã bị thay đổi. Ta có thể sẽ có ý định dùng useEffect
để theo dõi sự thay đổi đó và gọi callback của parent component để thông báo như sau:
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false)
// 🔴 Avoid: The onChange handler runs too late
useEffect(() => {
onChange(isOn)
}, [isOn, onChange])
function handleClick() {
setIsOn(!isOn)
}
// ...
}
Hàm onChange
trong đoạn code trên có nhiệm vụ thay đổi state của parent component và do đó nó cũng làm component Toggle
bị re-render.
Khi người dùng toggle thì sẽ có 2 lần render xảy ra:
setIsOn ➡️ render ➡️ onChange ➡️ render
Để giải quyết vấn đề trên, ta có thể thay đổi cả state của child component và state của parent component ở trong một event handler:
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false)
function updateToggle(nextIsOn) {
// ✅ Good: Perform all updates during the event that caused them
setIsOn(nextIsOn)
onChange(nextIsOn)
}
function handleClick() {
updateToggle(!isOn)
}
// ...
}
Thậm chí, ta cũng có thể xóa state isOn
ở trong child component và nhận nó từ parent component như sau:
// ✅ Also good: the component is fully controlled by its parent
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn)
}
// ...
}
Do cơ chế gom nhóm các state update2 nên sẽ chỉ có một lần re-render.
Fetching Data
Ta thường dùng useEffect
để lấy dữ liệu từ API:
function SearchResults({ query }) {
const [results, setResults] = useState([])
const [page, setPage] = useState(1)
useEffect(() => {
// 🔴 Avoid: Fetching without cleanup logic
fetchResults(query, page).then((json) => {
setResults(json)
})
}, [query, page])
function handleNextPageClick() {
setPage(page + 1)
}
// ...
}
Giả sử ta nhập vào ô tìm kiếm chữ "hello"
, sẽ có 5 request được gửi đi: "h"
, "he"
, "hel"
, "hell"
và "hello"
. Sẽ không có gì đảm bảo rằng các response sẽ được trả về theo đúng thứ tự request, chẳng hạn response của "hell"
có thể được trả về trước response của "hello"
.
Đây chính hiện tượng race condition: hai request khác nhau đua với nhau và response trả về của chúng có thể có thứ tự không như mong muốn.
Để giải quyết race condition, ta cần dọn dẹp request cũ trước khi thực hiện request mới bằng cách dùng cleanup function như sau:
function SearchResults({ query }) {
const [results, setResults] = useState([])
const [page, setPage] = useState(1)
useEffect(() => {
let ignore = false
fetchResults(query, page).then((json) => {
if (!ignore) {
setResults(json)
}
})
return () => {
ignore = true
}
}, [query, page])
function handleNextPageClick() {
setPage(page + 1)
}
// ...
}
Bằng cách này, ta có thể đảm bảo được tất cả các request đều bị từ chối trừ request cuối cùng.
Note
Cleanup function sẽ được gọi với các biến của effect cũ nên khi nó gán
ignore = true
thì tức là nó đã gán biếnignore
của request trước.
Related
list
from [[You Might Not Need an Effect]]
sort file.ctime asc
Resources
Footnotes
-
xem thêm Keeping Components Pure. ↩
-
xem thêm State as a Snapshot ↩