Trong bài viết này, tôi sẽ hướng dẫn bạn cách sử dụng React để thay thế useEffect trong hầu hết các trường hợp.
Tôi đã xem "Goodbye, useEffect" của David Khoursid và nó 🤯 khiến tôi suy nghĩ 😀 theo một cách tốt. Tôi đồng ý rằng useEffect đã được sử dụng nhiều đến mức làm cho mã của chúng ta trở nên bẩn và khó bảo trì. Tôi đã sử dụng useEffect trong một thời gian dài và tôi thấy tội lỗi vì đã lạm dụng nó. Tôi chắc rằng React có các tính năng giúp mã của tôi sạch hơn và dễ bảo trì hơn.
useEffect là hook cho phép chúng ta thực hiện các side effect trong function component. Nó kết hợp componentDidMount, componentDidUpdate và componentWillUnmount trong một API duy nhất. Đó là một cái móc hấp dẫn sẽ cho phép chúng ta làm được nhiều thứ. Nhưng nó cũng là một cái móc rất nguy hiểm có thể gây ra rất nhiều sâu bọ.
Hãy cùng xem 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> }
Đó là một bộ đếm đơn giản tăng lên mỗi giây. Nó sử dụng useEffect để đặ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 ngắt kết nối. Đoạn mã trên là một trường hợp sử dụng rộng rãi cho useEffect.
Đó là một ví dụ đơn giản, nhưng nó cũng là một ví dụ tồi tệ.
Vấn đề với ví dụ này là khoảng thời gian được đặt mỗi khi thành phần kết xuất lại. Nếu thành phần kết xuất lại vì bất kỳ lý do gì, khoảng thời gian sẽ được đặt lại. Khoảng thời gian sẽ được gọi hai lần mỗi giây. Đó không phải là vấn đề với ví dụ đơn giản này, nhưng nó có thể là vấn đề lớn khi khoảng phức tạp hơn. Nó cũng có thể gây rò rỉ bộ nhớ.
Làm thế nào để sửa chữa nó?
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> }
Đoạn mã trên tốt hơn rất nhiều so với ví dụ trước. Nó không đặt khoảng thời gian mỗi khi thành phần kết xuất lại. Nhưng nó vẫn cần cải thiện. Nó vẫn còn một chút phức tạp. Và nó vẫn sử dụng useEffect, đây là một hook rất nguy hiểm.
Như chúng ta đã biết về useEffect, nó kết hợp componentDidMount, componentDidUpdate và componentWillUnmount trong một API duy nhất. Hãy đưa ra một số ví dụ về nó:
useEffect(() => { // componentDidMount? }, [])
useEffect(() => { // componentDidUpdate? }, [something, anotherThing])
useEffect(() => { return () => { // componentWillUnmount? } }, [])
Thật dễ dàng để hiểu. useEffect được sử dụng để thực hiện các tác dụng phụ khi thành phần gắn kết, cập nhật và ngắt kết nối. 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 kết xuất lại. Không nên thực hiện các tác dụng phụ khi thành phần kết xuất lại. Nó có thể gây ra rất 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 kết xuất lại.
useEffect không phải là móc vòng đờ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>Number of changes: {count}</div> </div> ) }
useEffect không phải là trình thiết lập trạng thái
import React, { useState, useEffect } from 'react' const Example = () => { const [count, setCount] = useState(0) // Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times` }) // <-- this is the problem, 😱 it's missing the dependency array return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</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
Bắt buộc : Khi có điều gì đó xảy ra, hãy thực hiện hiệu ứng này.
Khai báo : Khi một đ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 phần nào của trạng thái đã thay đổi, hiệu ứng 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 lại nó mà không cần lý do kết xuất đồng thời.
Khái niệm :
useEffect(() => { doSomething() return () => cleanup() }, [whenThisChanges])
Thực hiện :
useEffect(() => { if (foo && bar && (baz || quo)) { doSomething() } else { doSomethingElse() } // oops, I forgot the cleanup }, [foo, bar, baz, quo])
Triển khai 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])
Thật đáng sợ khi viết mã này. Hơn nữa, nó sẽ bình thường trong cơ sở mã của chúng tôi và bị rối tung lên. 😱🤮
React 18 chạy hiệu ứng hai lần trên mount (ở chế độ nghiêm ngặt). Gắn/hiệu ứng (╯°□°)╯︵ ┻━┻ -> Ngắt kết nối (mô phỏng)/dọn dẹp ┬─┬ /( º _ º /) -> Gắn lại/hiệu ứng (╯°□°)╯︵ ┻━┻
Nó có nên được đặt bên ngoài thành phần? useEffect mặc định? Ờ... lúng túng. Hmm... 🤔 Chúng tôi không thể đưa nó vào kết xuất vì nó sẽ không có tác dụng phụ ở đó vì kết xuất 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.
đồng bộ hóa
useEffect(() => { const sub = createThing(input).subscribe((value) => { // do something with value }) 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) } }, [])
Fire-and-forget Synchronized (Action effects) (Activity effects) 0 ---------------------- ----------------- - - - oo | A | oo | A | A oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo | | | oo | | | | oo V | V oo V | V | o--------------------------------------------------------------------------------> Unmount Remount
Trình xử lý sự kiện. sắp xếp.
<form onSubmit={(event) => { // 💥 side-effect! 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à "Trình xử lý sự kiện có thể có tác dụng phụ không?" một phần .
Chắc chắn rồi! <u>Trình xử lý sự kiện là nơi tốt nhất cho các tác dụng phụ.</u>
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, <u>các tác dụng phụ thường thuộc về các trình xử lý sự kiện bên trong.</u>
Nếu bạn đã sử dụng hết tất cả các tùy chọn khác và không thể tìm thấy trình xử lý sự kiện phù hợp cho tác dụng phụ của mình, thì bạn vẫn có thể đính kèm nó vào JSX đã trả về bằng lệnh gọi <u>useEffect</u> trong thành phần của mình. Điều này yêu cầu React thực thi nó sau, sau khi kết xuất, khi các tác dụng phụ được cho phép. <u> Tuy nhiên, phương pháp này nên là phương sách cuối cùng của bạn. </u>
"Các hiệu ứng xảy ra bên ngoài kết xuất" - David Khoursid.
(state) => UI (state, event) => nextState // 🤔 Effects?
UI là một chức năng của trạng thái. Khi tất cả các trạng thái hiện tại được hiển thị, nó sẽ tạo giao diện người dùng hiện tại. Tương tự như vậy, 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 giao diện người dùng mới. Mô hình này là cốt lõi của React.
Phần mềm trung gian? 🕵️ Gọi lại? 🤙 Truyện cổ tích? 🧙♂️ Phản ứng? 🧪 Chìm? 🚰 Monads(?) 🧙♂️ Bất cứ khi nào? 🤷♂️
Chuyển trạng thái. Luôn.
(state, event) => nextState | V (state, event) => (nextState, effect) // Here
Hiệu ứng hành động đi đâu? Trình xử lý sự kiện. Chuyển trạng thái.
Điều này xảy ra để được thực thi cùng lúc với trình xử lý sự kiện.
Chúng tôi có thể sử dụng useEffect vì chúng tôi không biết rằng đã có API tích hợp từ React có thể giải quyết vấn đề này.
Đây là một nguồn tuyệt vời để đọc về chủ đề này: Bạn có thể không cần một hiệu ứng
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]) // ... }
Hãy đọc và suy nghĩ lại cho kỹ 🧐.
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
để ghi nhớ 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 sử dụng useMemo
để ghi nhớ nó vì về cơ bản, chúng ta đang đánh đổi hiệu suất lấy bộ nhớ.
Bất cứ khi nào chúng ta thấy setState
trong useEffect
, đó là một dấu hiệu cảnh báo rằng chúng ta có thể đơn giản hóa nó.
useEffect ➡️ useSyncExternalStore
❌ Sai cách:
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( // 👇 subscribe storeApi.subscribe, // 👇 get snapshot () => storeApi.getStatus() === 'connected', // 👇 get server snapshot true ) // ... }
useEffect ➡️ eventHandler
❌ Sai cách:
const ChildProduct = ({ onOpen, onClose }) => { const [isOpen, setIsOpen] = useState(false) useEffect(() => { if (isOpen) { onOpen() } else { onClose() } }, [isOpen]) return ( <div> <button onClick={() => { setIsOpen(!isOpen) }} > Toggle quick view </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={} > Toggle quick view </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} > Toggle quick view </button> </div> ) }
useEffect ➡️ justCallIt
❌ Sai cách:
const Store = () => { useEffect(() => { storeApi.authenticate() // 👈 This will run twice! }, []) // ... }
🔨 Hãy sửa nó:
const Store = () => { const didAuthenticateRef = useRef() useEffect(() => { if (didAuthenticateRef.current) return storeApi.authenticate() didAuthenticateRef.current = true }, []) // ... }
➿ Cách khác:
let didAuthenticate = false const Store = () => { useEffect(() => { if (didAuthenticate) return storeApi.authenticate() didAuthenticate = true }, []) // ... }
🤔 Làm thế nào nếu:
storeApi.authenticate() const Store = () => { // ... }
🍷 SSR hả bạn?
if (typeof window !== 'undefined') { storeApi.authenticate() } const Store = () => { // ... }
🧪 Thử nghiệm?
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.
useEffect ➡️ renderAsYouFetch (SSR) hoặc useSWR (CSR)
❌ Sai cách:
const Store = () => { const [items, setItems] = useState([]) useEffect(() => { let isCanceled = false getItems().then((data) => { if (isCanceled) return setItems(data) }) return () => { isCanceled = true } }) // ... }
💽 Cách phối lại:
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 Thành phần máy chủ theo cách:
// app/page.tsx async function getData() { const res = await fetch('https://api.example.com/...') // The return value is *not* serialized // You can return Date, Map, Set, etc. // Recommendation: handle errors if (!res.ok) { // This will activate the closest `error.js` Error Boundary throw new Error('Failed to fetch data') } return res.json() } export default async function Page() { const data = await getData() return <main></main> }
⏭️💁 Next.js (appDir) với useSWR trong Thành phần máy khách theo cách:
// app/page.tsx import useSWR from 'swr' export default function Page() { const { data, error } = useSWR('/api/data', fetcher) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data}!</div> }
⏭️🧹 Next.js (pagesDir) theo cách 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>hello {data}!</div> }
⏭️💁 Next.js (pagesDir) theo cách CSR:
// pages/index.tsx import useSWR from 'swr' export default function Page() { const { data, error } = useSWR('/api/data', fetcher) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data}!</div> }
🍃 Truy vấn phản ứng (cách SSR:
import { getItems } from './storeApi' import { useQuery } from 'react-query' const Store = () => { const queryClient = useQueryClient() return ( <button onClick={() => { queryClient.prefetchQuery('items', getItems) }} > See items </button> ) } const Items = () => { const { data, isLoading, isError } = useQuery('items', getItems) // ... }
⁉️ Thật đấy ⁉️
Chúng ta nên sử dụng cái gì? sử dụng Tác dụng? sử dụngTruy vấn? sử dụngSWR?
hoặc... chỉ cần sử dụng() 🤔
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 React RFC.
function Note({ id }) { // This fetches a note asynchronously, but to the component author, it looks // like a synchronous operation. const note = use(fetchNote(id)) return ( <div> <h1>{note.title}</h1> <section>{note.body}</section> </div> ) }
🏃♂️ Điều kiện cuộc đ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
🌊 Đuổi thác
- Reddit, Dan Abramov
Từ tìm nạp dữ liệu đến chiến đấu với các API bắt buộc, tác dụng phụ là một trong những nguyên nhân gây thất vọng đáng kể nhất trong quá trình phát triển ứng dụng web. Và thành thật mà nói, việc đưa mọi thứ vào sử dụng Móc hiệu ứng chỉ giúp ích một chút. Rất may, có một khoa học (tốt, toán học) đối với các tác dụng phụ, được chính thức hóa trong các máy trạng thái và biểu đồ trạng thái, có thể giúp chúng ta lập mô hình trực quan và hiểu cách phối hợp các hiệu ứng, bất kể chúng phức tạp đến mức nào theo cách khai báo.