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 firstNamelastName 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 (todosfilter) 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 useStateuseEffect 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""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ến ignore của request trước.

list
from [[You Might Not Need an Effect]]
sort file.ctime asc

Resources

Footnotes

  1. xem thêm Keeping Components Pure.

  2. xem thêm State as a Snapshot