- Đăng ngày
Tạm biệt React, Tạm biệt useEffect (Hy vọng)
- Tác giả
- Tên
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
Thay thế useEffect bằng các hook khác trong React
Bài viết này sẽ hướng dẫn bạn cách sử dụng các hook khác trong React thay thế cho useEffect trong hầu hết các trường hợp.
Tôi đã xem video "Goodbye, useEffect" của David Khoursid, và nó thực sự 🤯 khiến tôi bất ngờ theo cách 😀 tích cực. Tôi đồng ý rằng useEffect đã được sử dụng quá nhiều, khiến code của chúng ta trở nên rối rắm và khó bảo trì. Bản thân tôi cũng đã sử dụng useEffect trong một thời gian dài, và tôi đã mắc lỗi lạm dụng nó. Tôi chắc chắn rằng React có các tính năng sẽ giúp code của tôi sạch hơn và dễ bảo trì hơn.
useEffect là gì?
useEffect là một hook cho phép chúng ta thực hiện các tác dụng phụ trong các thành phần hàm. Nó kết hợp componentDidMount, componentDidUpdate và componentWillUnmount thành một API duy nhất. Đây là một hook mạnh mẽ giúp chúng ta thực hiện nhiều thứ. Tuy nhiên, nó cũng là một hook rất nguy hiểm có thể gây ra nhiều lỗi.
Tại sao useEffect lại nguy hiểm?
Hãy xem xét ví dụ sau:
import React, { useEffect } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(interval)
}, [])
return <div>{count}</div>
}
Đây là một bộ đếm đơn giản tăng lên mỗi giây. Nó sử dụng useEffect để thiết lập một khoảng thời gian. Nó cũng sử dụng useEffect để xóa khoảng thời gian khi thành phần bị gỡ bỏ. Code snippet trên là một trường hợp sử dụng phổ biến cho useEffect. Nó là một ví dụ đơn giản, nhưng cũng là một ví dụ tồi tệ.
Vấn đề của ví dụ này là khoảng thời gian được thiết lập mỗi khi thành phần được render lại. Nếu thành phần được render lại vì bất kỳ lý do nào, khoảng thời gian sẽ được thiết lập lại. Khoảng thời gian sẽ được gọi hai lần mỗi giây. Điều này không phải là vấn đề với ví dụ đơn giản này, nhưng nó có thể là một vấn đề lớn khi khoảng thời gian phức tạp hơn. Nó cũng có thể gây ra rò rỉ bộ nhớ.
Cách khắc phục?
Có nhiều cách để khắc phục vấn đề này. Một cách là sử dụng useRef để lưu trữ khoảng thời gian.
import React, { useEffect, useRef } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const intervalRef = useRef()
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
return <div>{count}</div>
}
Code trên tốt hơn nhiều so với ví dụ trước. Nó không thiết lập khoảng thời gian mỗi khi thành phần được render lại. Nhưng nó vẫn cần cải thiện. Nó vẫn còn hơi phức tạp. Và nó vẫn sử dụng useEffect, một hook rất nguy hiểm.
useEffect không dành cho tác dụng phụ
Như chúng ta biết về useEffect, nó kết hợp componentDidMount, componentDidUpdate và componentWillUnmount thành một API duy nhất. Hãy xem một số ví dụ:
useEffect(() => {
// componentDidMount?
}, [])
useEffect(() => {
// componentDidUpdate?
}, [something, anotherThing])
useEffect(() => {
return () => {
// componentWillUnmount?
}
}, [])
Rất dễ hiểu. useEffect được sử dụng để thực hiện các tác dụng phụ khi thành phần được gắn, cập nhật và gỡ bỏ. Nhưng nó không chỉ được sử dụng để thực hiện các tác dụng phụ. Nó cũng được sử dụng để thực hiện các tác dụng phụ khi thành phần được render lại. Không nên thực hiện các tác dụng phụ khi thành phần được render lại. Điều này có thể gây ra nhiều lỗi. Tốt hơn là sử dụng các hook khác để thực hiện các tác dụng phụ khi thành phần được render lại.
import React, { useState, useEffect } from 'react'
const Example = () => {
const [value, setValue] = useState('')
const [count, setCount] = useState(-1)
useEffect(() => {
setCount(count + 1)
})
const onChange = ({ target }) => setValue(target.value)
return (
<div>
<input type="text" value={value} onChange={onChange} />
<div>Số lần thay đổi: {count}</div>
</div>
)
}
useEffect không phải là một bộ đặt trạng thái
import React, { useState, useEffect } from 'react'
const Example = () => {
const [count, setCount] = useState(0)
// Tương tự như componentDidMount và componentDidUpdate:
useEffect(() => {
// Cập nhật tiêu đề tài liệu bằng API của trình duyệt
document.title = `Bạn đã nhấp ${count} lần`
}) // <-- đây là vấn đề, 😱 nó thiếu mảng phụ thuộc
return (
<div>
<p>Bạn đã nhấp {count} lần</p>
<button onClick={() => setCount(count + 1)}>Nhấp vào tôi</button>
</div>
)
}
Tôi khuyên bạn nên đọc tài liệu này: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Lệnh vs Khai báo
Lệnh: Khi điều gì đó xảy ra, hãy thực thi tác dụng phụ này.
Khai báo: Khi điều gì đó xảy ra, nó sẽ khiến trạng thái thay đổi và tùy thuộc (mảng phụ thuộc) vào các phần của trạng thái đã thay đổi, tác dụng phụ này sẽ được thực thi, nhưng chỉ khi một số điều kiện là đúng. Và React có thể thực thi nó lại một lần nữa mà không cần lý do để render đồng thời.
Khái niệm vs Thực thi
Khái niệm:
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
Thực thi:
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// ôi, tôi quên phần dọn dẹp
}, [foo, bar, baz, quo])
Thực thi trong thế giới thực:
useEffect(() => {
if (isOpen && component && containerElRef.current) {
if (React.isValidElement(component)) {
ionContext.addOverlay(overlayId, component, containerElRef.current!);
} else {
const element = createElement(component as React.ComponentClass, componentProps);
ionContext.addOverlay(overlayId, element, containerElRef.current!);
}
}
}, [component, containerElRef.current, isOpen, componentProps]);
useEffect(() => {
if (removingValue && !hasValue && cssDisplayFlex) {
setCssDisplayFlex(false)
}
setRemovingValue(false)
}, [removingValue, hasValue, cssDisplayFlex])
Viết code này rất đáng sợ. Hơn nữa, nó sẽ trở nên bình thường trong codebase của chúng ta và bị rối tung lên. 😱🤮
Tác dụng phụ nên được đặt ở đâu?
React 18 thực thi tác dụng phụ hai lần khi gắn (trong chế độ nghiêm ngặt). Gắn/tác dụng phụ (╯°□°)╯︵ ┻━┻ -> Gỡ bỏ (mô phỏng)/dọn dẹp ┬─┬ /( º _ º /) -> Gắn lại/tác dụng phụ (╯°□°)╯︵ ┻━┻
Nó nên được đặt bên ngoài thành phần? useEffect mặc định? Uh... ngượng ngùng. Hmm... 🤔 Chúng ta không thể đặt nó vào render vì nó không nên có tác dụng phụ ở đó bởi vì render chỉ giống như vế phải của một phương trình toán học. Nó chỉ nên là kết quả của phép tính.
useEffect dùng để làm gì?
Đồng bộ hóa
useEffect(() => {
const sub = createThing(input).subscribe((value) => {
// làm gì đó với giá trị
})
return sub.unsubscribe
}, [input])
useEffect(() => {
const handler = (event) => {
setPointer({ x: event.clientX, y: event.clientY })
}
elRef.current.addEventListener('pointermove', handler)
return () => {
elRef.current.removeEventListener('pointermove', handler)
}
}, [])
Tác dụng hành động vs Tác dụng hoạt động
Bắn và quên Đồng bộ hóa
(Tác dụng hành động) (Tác dụng hoạt động)
0 ---------------------- ----------------- - - -
o o | A | o o | A | A
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o V | V o o V | V |
o-------------------------------------------------------------------------------->
Gỡ bỏ Gắn lại
Tác dụng hành động nên được đặt ở đâu?
Bộ xử lý sự kiện. Loại là vậy.
<form
onSubmit={(event) => {
// 💥 tác dụng phụ!
submitData(event)
}}
>
{/* ... */}
</form>
Có thông tin tuyệt vời trong Beta React.js. Tôi khuyên bạn nên đọc nó. Đặc biệt là phần "Bộ xử lý sự kiện có thể có tác dụng phụ không?".
Hoàn toàn! Bộ xử lý sự kiện là nơi tốt nhất cho các tác dụng phụ.
Một tài nguyên tuyệt vời khác mà tôi muốn đề cập là Nơi bạn có thể gây ra tác dụng phụ
Trong React, các tác dụng phụ thường nằm trong các bộ xử lý sự kiện.
Nếu bạn đã thử hết mọi cách khác và không tìm thấy bộ xử lý sự kiện phù hợp cho tác dụng phụ của mình, bạn vẫn có thể gắn nó vào JSX đã trả về với cuộc gọi useEffect trong thành phần của mình. Điều này cho React biết để thực thi nó sau đó, sau khi render, khi các tác dụng phụ được phép. Tuy nhiên, phương pháp này nên là phương án cuối cùng của bạn.
"Tác dụng phụ xảy ra bên ngoài quá trình render" - David Khoursid.
(state) => UI
(state, event) => nextState // 🤔 Tác dụng phụ?
UI là một hàm của trạng thái. Khi tất cả các trạng thái hiện tại được render, nó sẽ tạo ra UI hiện tại. Tương tự, khi một sự kiện xảy ra, nó sẽ tạo ra một trạng thái mới. Và khi trạng thái thay đổi, nó sẽ xây dựng một UI mới. Nguyên tắc này là cốt lõi của React.
Khi nào tác dụng phụ xảy ra?
Middleware? 🕵️ Callbacks? 🤙 Sagas? 🧙♂️ Reactions? 🧪 Sinks? 🚰 Monads(?) 🧙♂️ Bất cứ khi nào? 🤷♂️
Chuyển đổi trạng thái. Luôn luôn.
(state, event) => nextState
|
V
(state, event) => (nextState, effect) // Ở đây
![Hình minh họa render lại](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")
Tác dụng hành động nên được đặt ở đâu? Bộ xử lý sự kiện. Chuyển đổi trạng thái.
Điều này xảy ra đồng thời với bộ xử lý sự kiện.
Chúng ta có thể không cần tác dụng phụ
Chúng ta có thể sử dụng useEffect bởi vì chúng ta không biết rằng đã có API tích hợp sẵn của React có thể giải quyết vấn đề này.
Đây là một tài nguyên tuyệt vời để đọc về chủ đề này: Bạn có thể không cần tác dụng phụ
Chúng ta không cần useEffect để chuyển đổi dữ liệu.
useEffect ➡️ useMemo (mặc dù chúng ta không cần useMemo trong hầu hết các trường hợp)
const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])
// ...
}
Đọc và suy nghĩ lại cẩn thận 🧐.
const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])
// ...
}
Thay vì sử dụng useEffect
để tính tổng, chúng ta có thể sử dụng useMemo
để lưu trữ tổng. Ngay cả khi biến không phải là một phép tính đắt tiền, chúng ta không cần phải sử dụng useMemo
để lưu trữ nó bởi vì về cơ bản chúng ta đang trao đổi hiệu suất để lấy bộ nhớ.
Bất cứ khi nào chúng ta thấy setState
trong useEffect
, đó là dấu hiệu cảnh báo rằng chúng ta có thể đơn giản hóa nó.
useSyncExternalStore
Tác dụng phụ với các kho lưu trữ bên ngoài?useEffect ➡️ useSyncExternalStore
❌ Cách sai:
const Store = () => {
const [isConnected, setIsConnected] = useState(true)
useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected')
})
return () => {
sub.unsubscribe()
}
}, [])
// ...
}
✅ Cách tốt nhất:
const Store = () => {
const isConnected = useSyncExternalStore(
// 👇 đăng ký
storeApi.subscribe,
// 👇 nhận ảnh chụp nhanh
() => storeApi.getStatus() === 'connected',
// 👇 nhận ảnh chụp nhanh từ máy chủ
true
)
// ...
}
Chúng ta không cần useEffect để giao tiếp với cha mẹ.
useEffect ➡️ bộ xử lý sự kiện
❌ Cách sai:
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
if (isOpen) {
onOpen()
} else {
onClose()
}
}, [isOpen])
return (
<div>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
>
Bật/tắt xem nhanh
</button>
</div>
)
}
📈 Cách tốt hơn:
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen;
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return (
<div>
<button
onClick={}
>
Bật/tắt xem nhanh
</button>
</div>
)
}
✅ Cách tốt nhất là tạo một hook tùy chỉnh:
const useToggle({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return [isOpen, handleToggle]
}
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, handleToggle] = useToggle({ onOpen, onClose })
return (
<div>
<button
onClick={handleToggle}
>
Bật/tắt xem nhanh
</button>
</div>
)
}
Chúng ta không cần useEft để khởi tạo các đối tượng đơn lẻ toàn cầu.
useEffect ➡️ justCallIt
❌ Cách sai:
const Store = () => {
useEffect(() => {
storeApi.authenticate() // 👈 Điều này sẽ chạy hai lần!
}, [])
// ...
}
🔨 Hãy sửa nó:
const Store = () => {
const didAuthenticateRef = useRef()
useEffect(() => {
if (didAuthenticateRef.current) return
storeApi.authenticate()
didAuthenticateRef.current = true
}, [])
// ...
}
➿ Một cách khác:
let didAuthenticate = false
const Store = () => {
useEffect(() => {
if (didAuthenticate) return
storeApi.authenticate()
didAuthenticate = true
}, [])
// ...
}
🤔 Làm sao nếu:
storeApi.authenticate()
const Store = () => {
// ...
}
🍷 SSR, đúng không?
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
const Store = () => {
// ...
}
🧪 Kiểm tra?
const renderApp = () => {
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
appRoot.render(<Store />)
}
Chúng ta không nhất thiết phải đặt mọi thứ bên trong một thành phần.
Chúng ta không cần useEffect để lấy dữ liệu.
useEffect ➡️ renderAsYouFetch (SSR) hoặc useSWR (CSR)
❌ Cách sai:
const Store = () => {
const [items, setItems] = useState([])
useEffect(() => {
let isCanceled = false
getItems().then((data) => {
if (isCanceled) return
setItems(data)
})
return () => {
isCanceled = true
}
})
// ...
}
💽 Cách của Remix:
import { useLoaderData } from '@renix-run/react'
import { json } from '@remix-run/node'
import { getItems } from './storeApi'
export const loader = async () => {
const items = await getItems()
return json(items)
}
const Store = () => {
const items = useLoaderData()
// ...
}
export default Store
⏭️🧹 Next.js (appDir) với async/await trong Server Component:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// Giá trị trả về *không* được nối tiếp
// Bạn có thể trả về Date, Map, Set, v.v.
// Khuyến nghị: xử lý lỗi
if (!res.ok) {
// Điều này sẽ kích hoạt `error.js` Error Boundary gần nhất
throw new Error('Lỗi khi lấy dữ liệu')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
⏭️💁 Next.js (appDir) với useSWR trong Client Component:
// app/page.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>Lỗi khi tải</div>
if (!data) return <div>Đang tải...</div>
return <div>xin chào {data}!</div>
}
⏭️🧹 Next.js (pagesDir) trong SSR:
// pages/index.tsx
import { GetServerSideProps } from 'next'
export const getServerSideProps: GetServerSideProps = async () => {
const res = await fetch('https://api.example.com/...')
const data = await res.json()
return {
props: {
data,
},
}
}
export default function Page({ data }) {
return <div>xin chào {data}!</div>
}
⏭️💁 Next.js (pagesDir) trong CSR:
// pages/index.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>Lỗi khi tải</div>
if (!data) return <div>Đang tải...</div>
return <div>xin chào {data}!</div>
}
🍃 React Query (SSR):
import { getItems } from './storeApi'
import { useQuery } from 'react-query'
const Store = () => {
const queryClient = useQueryClient()
return (
<button
onClick={() => {
queryClient.prefetchQuery('items', getItems)
}}
>
Xem các mục
</button>
)
}
const Items = () => {
const { data, isLoading, isError } = useQuery('items', getItems)
// ...
}
⁉️ Thật sự ⁉️ Chúng ta nên sử dụng gì? useEffect? useQuery? useSWR?
hoặc... chỉ cần use() 🤔
use() là một hàm React mới chấp nhận một lời hứa về mặt khái niệm tương tự như await. use() xử lý lời hứa được trả về bởi một hàm theo cách tương thích với các thành phần, hook và Suspense. Tìm hiểu thêm về use() trong RFC của React.
function Note({ id }) {
// Điều này lấy một ghi chú một cách bất đồng bộ, nhưng đối với tác giả thành phần, nó trông
// giống như một hoạt động đồng bộ.
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
Vấn đề khi lấy dữ liệu trong useEffect
🏃♂️ Điều kiện chạy đua
🔙 Không có nút quay lại ngay lập tức
🔍 Không có SSR hoặc nội dung HTML ban đầu
🌊 Theo đuổi thác nước
- Reddit, Dan Abramov
Kết luận
Từ việc lấy dữ liệu đến việc chống lại các API mệnh lệnh, các tác dụng phụ là một trong những nguồn gây bực bội nhất trong phát triển ứng dụng web. Và hãy thành thật mà nói, việc đặt mọi thứ vào các hook useEffect chỉ giúp ích một chút. May mắn thay, có một khoa học (vâng, toán học) cho các tác dụng phụ, được chính thức hóa trong các máy trạng thái và statecharts, có thể giúp chúng ta mô hình hóa và hiểu rõ cách phối hợp các tác dụng phụ, bất kể chúng phức tạp như thế nào một cách khai báo.