在本文中,我将向您展示如何在大多数情况下使用 React 来替代 useEffect。
我一直在看David Khoursid 的“再见,useEffect” ,它 🤯 以一种很好的方式让我大吃一惊。我同意 useEffect 的使用太多以至于它使我们的代码变脏并且难以维护。我已经使用 useEffect 很长时间了,我对滥用它感到内疚。我确信 React 具有使我的代码更清晰、更易于维护的功能。
useEffect 是一个钩子,它允许我们在功能组件中执行副作用。它将 componentDidMount、componentDidUpdate 和 componentWillUnmount 组合在一个 API 中。这是一个引人注目的钩子,可以让我们做很多事情。但它也是一个非常危险的钩子,会导致很多错误。
让我们看看下面的例子:
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> }
这是一个每秒增加的简单计数器。它使用 useEffect 来设置间隔。它还使用 useEffect 来清除组件卸载时的间隔。上面的代码片段是 useEffect 的一个广泛使用案例。
这是一个简单的例子,但也是一个糟糕的例子。
这个例子的问题是每次组件重新渲染时都会设置间隔。如果组件因任何原因重新渲染,则将重新设置间隔。间隔将每秒调用两次。对于这个简单的例子来说这不是问题,但是当间隔更复杂时它可能是一个大问题。它还可能导致内存泄漏。
如何解决?
有很多方法可以解决这个问题。一种方法是使用 useRef 来存储间隔。
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> }
上面的代码比前面的例子好很多。它不会在每次组件重新渲染时设置间隔。但它仍然需要改进。还是有点复杂。而且它仍然使用 useEffect,这是一个非常危险的钩子。
正如我们对 useEffect 的了解,它将 componentDidMount、componentDidUpdate 和 componentWillUnmount 组合在一个 API 中。让我们举一些例子:
useEffect(() => { // componentDidMount? }, [])
useEffect(() => { // componentDidUpdate? }, [something, anotherThing])
useEffect(() => { return () => { // componentWillUnmount? } }, [])
理解起来毫不费力。 useEffect 用于在组件挂载、更新和卸载时执行副作用。但它不仅用于执行副作用。它还用于在组件重新呈现时执行副作用。在组件重新渲染时执行副作用不是一个好主意。它会导致很多错误。当组件重新渲染时,最好使用其他钩子来执行副作用。
useEffect 不是生命周期钩子。
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 不是状态设置器
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> ) }
我建议阅读此文档:https ://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
势在必行:当事情发生时,执行这个效果。
声明式:当某事发生时,它会导致状态发生变化,并且取决于(依赖数组)状态的哪些部分发生了变化,应该执行此效果,但前提是某些条件为真。而 React 可能会莫名其妙的再次执行并发渲染。
理念:
useEffect(() => { doSomething() return () => cleanup() }, [whenThisChanges])
实施:
useEffect(() => { if (foo && bar && (baz || quo)) { doSomething() } else { doSomethingElse() } // oops, I forgot the cleanup }, [foo, bar, baz, quo])
现实世界的实施:
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])
写这段代码很可怕。此外,它在我们的代码库中是正常的,并且会搞砸。 😱🤮
React 18 在挂载上运行 effects 两次(在严格模式下)。安装/效果(╯°□°)╯︵┻━┻->卸载(模拟)/清理┬─┬/(º_º/)->重新安装/效果(╯°□°)╯︵┻━┻
它应该放在组件外面吗?默认使用效果?呃……尴尬。嗯...🤔 我们不能把它放在渲染中,因为它应该没有副作用,因为渲染就像数学方程式的右边一样。应该只是计算的结果。
同步化
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
事件处理程序。排序。
<form onSubmit={(event) => { // 💥 side-effect! submitData(event) }} > {/* ... */} </form>
Beta React.js 中有很好的信息。我建议阅读它。特别是 “事件处理程序可以有副作用吗?”部分。
绝对地! <u>事件处理程序是处理副作用的最佳场所。</u>
我想提及的另一个重要资源是Where you can cause side effects
在 React 中,<u>副作用通常属于事件处理程序。</u>
如果您已用尽所有其他选项并且找不到适合您的副作用的事件处理程序,您仍然可以通过在组件中调用 <u>useEffect</u> 将其附加到返回的 JSX。这告诉 React 在渲染后允许副作用时稍后执行它。 <u>然而,这种方法应该是你最后的选择。 </u>
“效果发生在渲染之外” - David Khoursid。
(state) => UI (state, event) => nextState // 🤔 Effects?
UI 是状态的函数。当呈现所有当前状态时,它将生成当前 UI。同样,当一个事件发生时,它会创建一个新的状态。当状态发生变化时,它会构建一个新的 UI。这种范式是 React 的核心。
中间件? 🕵️回调? 🤙 传奇? 🧙♂️ 反应? 🧪 下沉? 🚰 Monads(?) 🧙♂️ 什么时候? 🤷♂️
状态转换。总是。
(state, event) => nextState | V (state, event) => (nextState, effect) // Here
动作效果去哪儿了?事件处理程序。状态转换。
恰好与事件处理程序同时执行。
我们可以使用 useEffect,因为我们不知道 React 已经有一个内置的 API 可以解决这个问题。
这是阅读有关此主题的绝佳资源:您可能不需要效果
useEffect ➡️ useMemo(尽管我们在大多数情况下不需要useMemo)
const Cart = () => { const [items, setItems] = useState([]) const [total, setTotal] = useState(0) useEffect(() => { setTotal(items.reduce((total, item) => total + item.price, 0)) }, [items]) // ... }
仔细阅读并再次思考🧐。
const Cart = () => { const [items, setItems] = useState([]) const total = useMemo(() => { return items.reduce((total, item) => total + item.price, 0) }, [items]) // ... }
我们可以使用useMemo
来记忆总数,而不是使用useEffect
来计算总数。即使变量不是一个昂贵的计算,我们也不需要使用useMemo
来记忆它,因为我们基本上是在用性能换取内存。
每当我们在useEffect
中看到setState
时,这是一个警告信号,表明我们可以简化它。
useEffect ➡️ useSyncExternalStore
❌错误的方式:
const Store = () => { const [isConnected, setIsConnected] = useState(true) useEffect(() => { const sub = storeApi.subscribe(({ status }) => { setIsConnected(status === 'connected') }) return () => { sub.unsubscribe() } }, []) // ... }
✅ 最佳方式:
const Store = () => { const isConnected = useSyncExternalStore( // 👇 subscribe storeApi.subscribe, // 👇 get snapshot () => storeApi.getStatus() === 'connected', // 👇 get server snapshot true ) // ... }
使用效果 ➡️ 事件处理器
❌错误的方式:
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> ) }
📈 更好的方法:
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> ) }
✅ 最好的方法是创建一个自定义钩子:
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
❌错误的方式:
const Store = () => { useEffect(() => { storeApi.authenticate() // 👈 This will run twice! }, []) // ... }
🔨 让我们修复它:
const Store = () => { const didAuthenticateRef = useRef() useEffect(() => { if (didAuthenticateRef.current) return storeApi.authenticate() didAuthenticateRef.current = true }, []) // ... }
➿ 另一种方式:
let didAuthenticate = false const Store = () => { useEffect(() => { if (didAuthenticate) return storeApi.authenticate() didAuthenticate = true }, []) // ... }
🤔 如果:
storeApi.authenticate() const Store = () => { // ... }
🍷SSR,嗯?
if (typeof window !== 'undefined') { storeApi.authenticate() } const Store = () => { // ... }
🧪 测试?
const renderApp = () => { if (typeof window !== 'undefined') { storeApi.authenticate() } appRoot.render(<Store />) }
我们不一定需要将所有内容都放在一个组件中。
useEffect ➡️ renderAsYouFetch (SSR) 或 useSWR (CSR)
❌错误的方式:
const Store = () => { const [items, setItems] = useState([]) useEffect(() => { let isCanceled = false getItems().then((data) => { if (isCanceled) return setItems(data) }) return () => { isCanceled = true } }) // ... }
💽 混音方式:
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) 以服务器组件方式使用 async/await:
// 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) 以客户端组件方式使用 useSWR:
// 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> }
⏭️🧹 SSR 方式的 Next.js (pagesDir):
// 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> }
⏭️💁 CSR 方式的 Next.js (pagesDir):
// 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> }
🍃 React 查询(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) // ... }
⁉️真的⁉️
我们应该使用什么?使用效果?使用查询?使用SWR?
或者...只需使用() 🤔
use() 是一个新的 React 函数,它接受概念上类似于 await 的承诺。 use() 以与组件、挂钩和 Suspense 兼容的方式处理函数返回的承诺。在 React RFC 中了解更多关于 use() 的信息。
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> ) }
🏃♂️比赛条件
🔙 没有即时后退按钮
🔍 无 SSR 或初始 HTML 内容
🌊 追瀑
- Reddit,丹·阿布拉莫夫
从获取数据到使用命令式 API,副作用是 Web 应用程序开发中最令人沮丧的原因之一。老实说,将所有东西都用在 useEffect 钩子上只会有一点帮助。值得庆幸的是,副作用有一门科学(嗯,数学),在状态机和状态图中形式化,可以帮助我们直观地建模和理解如何编排效果,无论它们以声明方式变得多么复杂。