фон
Я считаю, что у каждого есть такой опыт при написании React: в проекте используется много useEffect, так что наш код становится запутанным и сложным в обслуживании.
Может быть, хук useEffect не годится? Это не так, просто мы злоупотребляли этим.
В этой статье я покажу, как использовать другие методы вместо useEffect.
В чем пользаЭффект
useEffect позволяет выполнять побочные эффекты в функциональных компонентах. Он может имитировать componentDidMount, componentDidUpdate и componentWillUnmount. Мы можем сделать с ним много вещей. Но это также очень опасный хук, который может вызвать множество ошибок.
Почему useEffect подвержен ошибкам
Рассмотрим пример таймера:
import React, { useEffect } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
})
return <div>{count}</div>
}
复制代码
Это очень распространенный пример, но он очень плохой. Потому что, если компонент по какой-то причине перерендерится, таймер будет сброшен. Таймер будет вызываться два раза в секунду, что легко приведет к утечке памяти.
Как это исправить?
useRef
import React, { useEffect, useRef } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const timerRef = useRef()
useEffect(() => {
timerRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(timerRef.current)
}, [])
return <div>{count}</div>
}
复制代码
Он не устанавливает таймер каждый раз при повторном рендеринге компонента. Но такого простого кода у нас в проекте нет. Скорее, он находится в разных состояниях и делает разные вещи.
Вы думали, что написали useEffect
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
复制代码
На самом деле это так
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// 遗忘清理函数。
}, [foo, bar, baz, quo, ...])
复制代码
Напишите кучу логики, такой код очень грязный и сложный в обслуживании.
Для чего используется useEffect?
useEffect — это способ синхронизации React с какой-то внешней системой (сетью, подпиской, DOM). Если у вас нет внешних систем и вы просто пытаетесь управлять потоком данных с помощью useEffect, вы столкнетесь с проблемами.
Иногда нам не нужен useEffect
1. Нам не нужен useEffect для преобразования данных
const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])
// ...
}
复制代码
Приведенный выше код использует useEffect для преобразования данных, что очень неэффективно. На самом деле нет необходимости использовать useEffect. Когда какое-то значение можно вычислить из существующих реквизитов или состояния, не помещайте его в состояние, вычисляйте его во время рендеринга.
const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
const totalNum = items.reduce((total, item) => total + item.price, 0)
// ...
}
复制代码
Если логика расчета более сложная, можно использовать useMemo:
const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])
// ...
}
复制代码
2. Используйте useSyncExternalStore вместо useEffect
использованиеSyncExternalStore
Общий способ:
const Store = () => {
const [isConnected, setIsConnected] = useState(true)
useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected')
})
return () => {
sub.unsubscribe()
}
}, [])
// ...
}
复制代码
Лучший способ:
const Store = () => {
const isConnected = useSyncExternalStore(
storeApi.subscribe,
() => storeApi.getStatus() === 'connected',
true
)
// ...
}
复制代码
3. Нет необходимости использовать useEffect для связи с родительским компонентом
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>
)
}
复制代码
4.没必要使用useEffect初始化应用程序
const Store = () => {
useEffect(() => {
storeApi.authenticate()
}, [])
// ...
}
复制代码
更好的方式:
方式一:
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
}, [])
// ...
}
复制代码
方式三:
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
const Store = () => {
// ...
}
复制代码
5.没必要在useEffect请求数据
常见写法
const Store = () => {
const [items, setItems] = useState([])
useEffect(() => {
let isCanceled = false
getItems().then((data) => {
if (isCanceled) return
setItems(data)
})
return () => {
isCanceled = true
}
})
// ...
}
复制代码
更好的方式:
没有必要使用useEffect,可以使用swr:
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-query:
import { getItems } from './storeApi'
import { useQuery, useQueryClient } 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)
// ...
}
复制代码
没有正式发布的react的 use函数:
function Note({ id }) {
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
复制代码