- Опубликовано
Привет, React, привет useEffect (я надеюсь, что это так)
- Авторы
- Имя
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
В этой статье я покажу вам, как использовать React для замены useEffect в большинстве случаев.
Я смотрел видео "Прощай, useEffect" от Дэвида Хоурсида, и оно 🤯 просто взорвало мой мозг в 😀 хорошем смысле. Я согласен, что useEffect используется настолько часто, что делает наш код грязным и сложным в обслуживании. Я давно использую useEffect и виноват в том, что неправильно его применял. Я уверен, что у React есть функции, которые сделают мой код чище и легче в обслуживании.
Что такое useEffect?
useEffect — это хук, который позволяет нам выполнять побочные эффекты в функциональных компонентах. Он объединяет componentDidMount, componentDidUpdate и componentWillUnmount в едином API. Это мощный хук, который позволит нам сделать много вещей. Но он также очень опасный хук, который может привести к большому количеству ошибок.
Почему useEffect опасен?
Давайте посмотрим на следующий пример:
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 не предназначен для эффектов
Как мы знаем о useEffect, он объединяет componentDidMount, componentDidUpdate и componentWillUnmount в едином API. Давайте приведем несколько примеров этого:
useEffect(() => {
// componentDidMount?
}, [])
useEffect(() => {
// componentDidUpdate?
}, [something, anotherThing])
useEffect(() => {
return () => {
// componentWillUnmount?
}
}, [])
Это легко понять. 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)
// Аналогично componentDidMount и componentDidUpdate:
useEffect(() => {
// Обновление заголовка документа с помощью API браузера
document.title = `You clicked ${count} times`
}) // <-- это проблема, 😱 в нем отсутствует массив зависимостей
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
Императивный vs Декларативный
Императивный: Когда что-то происходит, выполните этот эффект.
Декларативный: Когда что-то происходит, это приведет к изменению состояния, и в зависимости (массив зависимостей) от того, какие части состояния изменились, этот эффект должен быть выполнен, но только если выполняется какое-то условие. И React может выполнить его снова без причины для конкурентной рендеризации.
Концепция vs Реализация
Концепция:
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
Реализация:
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// Ой, я забыл про очистку
}, [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 выполняет эффекты дважды при монтировании (в строгом режиме). Монтаж/эффект (╯°□°)╯︵ ┻━┻ -> Размонтирование (симулированное)/очистка ┬─┬ /( º _ º /) -> Повторный монтаж/эффект (╯°□°)╯︵ ┻━┻
Должен ли он располагаться вне компонента? По умолчанию useEffect? Э-э... неловко. Хмм... 🤔 Мы не можем поместить его в render, поскольку там не должно быть побочных эффектов, потому что render — это как правая часть математического уравнения. Это должен быть только результат вычислений.
Для чего нужен useEffect?
Синхронизация
useEffect(() => {
const sub = createThing(input).subscribe((value) => {
// сделать что-нибудь с 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)
}
}, [])
Эффекты действий vs Эффекты активности
Огонь и забудь Синхронизированные
(Эффекты действий) (Эффекты активности)
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-------------------------------------------------------------------------------->
Размонтирование Повторный монтаж
Куда же деваются эффекты действий?
Обработчики событий. Сорта.
<form
onSubmit={(event) => {
// 💥 побочный эффект!
submitData(event)
}}
>
{/* ... */}
</form>
В Beta React.js есть отличная информация. Я рекомендую ее прочитать. Особенно раздел "Могут ли обработчики событий иметь побочные эффекты?".
Конечно! Обработчики событий — лучшее место для побочных эффектов.
Еще один отличный ресурс, который я хочу упомянуть, — это Где вы можете вызывать побочные эффекты
В React побочные эффекты обычно находятся внутри обработчиков событий.
Если вы исчерпали все остальные варианты и не можете найти подходящий обработчик событий для своего побочного эффекта, вы все равно можете прикрепить его к возвращаемому JSX с помощью вызова useEffect в своем компоненте. Это говорит React о том, что нужно выполнить его позже, после рендеринга, когда разрешены побочные эффекты. Однако этот подход должен быть вашим последним средством.
"Эффекты происходят за пределами рендеринга" — Дэвид Хоурсид.
(state) => UI
(state, event) => nextState // 🤔 Эффекты?
UI — это функция от состояния. При рендеринге всех текущих состояний будет создан текущий UI. Точно так же при возникновении события будет создано новое состояние. И когда состояние изменится, будет создан новый UI. Эта парадигма лежит в основе React.
Когда же происходят эффекты?
Средний слой? 🕵️ Обратные вызовы? 🤙 Саги? 🧙♂️ Реакции? 🧪 Сливные элементы? 🚰 Монад(? ) 🧙♂️ Когда угодно? 🤷♂️
Переходы состояния. Всегда.
(state, event) => nextState
|
V
(state, event) => (nextState, effect) // Вот тут
![Иллюстрация перерендеринга](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")
Куда же деваются эффекты действий? Обработчики событий. Переходы состояния.
Что, оказывается, выполняется одновременно с обработчиками событий.
Возможно, нам и не нужны эффекты
Мы могли бы использовать useEffect, потому что мы не знали, что у React уже есть встроенный API, который может решить эту проблему.
Вот отличный ресурс, который стоит прочитать по этой теме: Вам, возможно, не нужен эффект
Нам не нужен useEffect для преобразования данных.
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])
// ...
}
Вместо использования useEffect
для вычисления общей суммы, мы можем использовать useMemo
для запоминания общей суммы. Даже если переменная не является дорогостоящим вычислением, нам не нужно использовать useMemo
для ее запоминания, потому что мы фактически меняем производительность на память.
Всякий раз, когда мы видим setState
в useEffect
, это предупреждающий сигнал о том, что мы можем упростить его.
useSyncExternalStore
Эффекты с внешними хранилищами?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(
// 👇 подписка
storeApi.subscribe,
// 👇 получение снимка
() => storeApi.getStatus() === 'connected',
// 👇 получение снимка сервера
true
)
// ...
}
Нам не нужен useEffect для общения с родителями.
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>
)
}
✅ Лучший способ — создать собственный хук:
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 для инициализации глобальных одиночек.
useEffect ➡️ justCallIt
❌ Неправильный способ:
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
}, [])
// ...
}
🤔 Как насчет:
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 для извлечения данных.
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
}
})
// ...
}
💽 Способ 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) с асинхронным/ожиданием в Server Component:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// Возвращаемое значение *не* сериализуется
// Вы можете вернуть Date, Map, Set и т. д.
// Рекомендация: обработка ошибок
if (!res.ok) {
// Это активирует ближайшую `error.js` границу ошибки
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 в Client Component:
// 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) в 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) в 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>
}
🍃 React Query (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)
// ...
}
⁉️ Действительно ⁉️ Что же нам использовать? useEffect? useQuery? useSWR?
или... просто use() 🤔
use() — это новая функция React, которая принимает обещание, концептуально похожее на await. use() обрабатывает обещание, возвращаемое функцией, таким образом, чтобы оно было совместимо с компонентами, хуками и Suspense. Подробнее о use() можно узнать в React RFC.
function Note({ id }) {
// Это асинхронно извлекает заметку, но для автора компонента это выглядит
// как синхронная операция.
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
Проблемы извлечения данных в useEffect
🏃♂️ Состояние гонки
🔙 Отсутствие мгновенной кнопки "Назад"
🔍 Отсутствие SSR или начального HTML-содержимого
🌊 Преследование водопада
- Reddit, Дэн Абрамов
Заключение
От извлечения данных до борьбы с императивными API, побочные эффекты являются одним из самых больших источников разочарования в разработке веб-приложений. И давайте будем честными, помещение всего в хуки useEffect помогает только немного. К счастью, существует наука (вернее, математика) побочных эффектов, формализованная в конечных автоматах и диаграммах состояний, которая может помочь нам визуально моделировать и понимать, как организовывать эффекты, независимо от того, насколько они сложны, декларативно.