В этой статье рассмотрим советы и приёмы, которые помогут более профессионально написать код на React.
Что такое useEffect?
useEffect — это хук, который можно использовать для замены некоторых методов жизненного цикла классового компонента. UseEffect используется с функциональными компонентами в следующих случаях:
при визуализации компонента (метод componentDidMount в классовом компоненте);
при обновлении компонента (метод componentDidUpdated в классовом компоненте);
при удалении компонента из DOM (метод componentWillUnmount в классовом компоненте).
Несколько побочных явлений:
Получение данных;
Прямое обновление DOM;
Установка заголовка страницы;
Работа с setInterval или setTimeout;
Измерение ширины, высоты или положения элементов в DOM;
Установка или получение значений в локальном хранилище.
Подписка на услуги и её отмена
Массив зависимостей useEffect
useEffect принимает два параметра. Первый аргумент — это функция обратного вызова, для которой мы будем выполнять побочные эффекты; другой – массив зависимостей. Второй аргумент является необязательным.
Если мы не передадим второй аргумент, побочный эффект в функции обратного вызова будет запускаться снова при каждой визуализации компонента.
function MyComponent() {
useEffect(() => {
// The side effect will run after every render
})
}
Если мы передаем второй аргумент в виде пустого массива, побочный эффект в функции обратного вызова сработает только один раз при первой визуализации компонента.
function MyComponent() {
useEffect(() => {
// This side effect will only run once, after the first render
}, [])
}
Если мы передаем свойства или значения состояния во втором аргументе, побочный эффект в функции обратного вызова будет выполняться только при изменении значений свойств или переменной состояния.
import { useEffect, useState } from 'react'
function MyComponent({ prop }) {
const [state, setState] = useState('')
useEffect(() => {
// the side effect will only run when the props or state changed
}, [prop, state])
}
Стоит отметить, что useEffect использует поверхностное (shallow) сравнение значений зависимостей.
Функция обратного вызова будет выполняться после каждой визуализации, если нет массива зависимостей.
Если есть пустой массив зависимостей, функция обратного вызова будет запущена только один раз после первой визуализации.
Если есть массив зависимостей со значениями свойств или переменной состояния, функция обратного вызова будет выполняться только при изменении этих значений.
Функция очистки useEffect
Второй аргумент useEffect позволяет запускать побочные эффекты при монтировании и обновлении компонента. Нам нужно запустить побочные эффекты и тогда, когда компонент размонтирован. Это функция очистки, которая позволяет нам остановить побочные эффекты непосредственно перед размонтированием компонента.
function MyComponent() {
useEffect(() => {
// this side effect will run after every render
return () => {
// this side effect will run before the component is unmounted
}
})
}
Пример реального использования
import { useEffect } from "react"
const Modal = ({ modalContent, closeModal }) => {
useEffect(() => {
let timeout = setTimeout(() => closeModal(), 3000)
return () => clearTimeout(timeout)
})
return (
<div className="modal">
<p>{modalContent}</p>
</div>
)
}
export default Modal
Что такое бесконечный цикл в useEffect?
Хотя useEffect используется повсеместно, для его освоения требуется некоторое время, поэтому многие новые пользователи при освоении useEffect могут получить бесконечный цикл. В этом разделе будут описаны распространенные сценарии, которые генерируют бесконечные циклы, и то, как их избежать.
Если не указан массив зависимостей:
function App() {
const [users, setUsers] = useState([])
useEffect(() => {
const getUsers = async () => {
const {data} = await axios.get("/api/user")
setUsers(data)
}
getUsers()
}) // without dependency array
}
Проблема и её решение
Пользовательское значение (состояние) изменяется при визуализации компонента. Поскольку состояние изменилось, компонент визуализируется. Поскольку мы не указали массив зависимостей, useEffect снова запускается, и состояние снова меняется.
function App() {
const [users, setUsers] = useState([])
useEffect(() => {
const getUsers = async () => {
const {data} = await axios.get("/api/user")
setUsers(data)
}
getUsers()
}, []) // empty dependency array
}
Если в массиве зависимостей указана функция:
function App() {
const [count, setCount] = useState(0)
function getResult() {
return 2 * 2
}
useEffect(() => {
setCount((count) => count + 1)
}, [getResult])
// we have specified a function in the dependency array
return (
<div>
<p>value of count: {count}</p>
</div>
)
}
export default App
Проблема и её решение
Мы знаем, что useEffect проводит поверхностные сравнения. Это делается, чтобы проверить, были ли обновлены зависимости. При использовании setCount состояние обновляется при первой визуализации компонента.
Как только состояние обновлено, компонент визуализируется снова. Поскольку getResult — это функция, контрольное значение в памяти воссоздается каждый раз при визуализации компонента, поэтому результат поверхностного сравнения возвращает false. Таким образом, образуется бесконечный цикл.
function App() {
const [count, setCount] = useState(0)
const getResult = useCallback(() => {
return 2 * 2
}, [])
useEffect(() => {
setCount((count) => count + 1)
}, [getResult])
// we have specified a function in the dependency array
return (
<div>
<p>value of count: {count}</p>
</div>
)
}
export default App
При использовании useCallback функция getResult запоминается. Это гарантирует, что контрольное значение функции getResult не изменится. Когда useEffect выполняет поверхностное сравнение, он возвращает true, и компонент не визуализируется.
Отметим, что существует множество способов избежать бесконечных циклов в компоненте. В статье рассказали только о нескольких.
Использование функций Async-Await в useEffect
Если мы хотим получать данные с помощью API, нам нужно выполнять асинхронные операции.
Как нам это делать с помощью useEffect?
Создайте асинхронную функцию вне useEffect и вызовите ее в useEffect.
const getPosts = async () => {
const {data} = await axios.get('api/posts')
setPosts(data)
}
useEffect(() => {
getUsers()
}, [])
Создайте асинхронную функцию в useEffect и вызовите ее в useEffect.
useEffect(() => {
const getPosts = async () => {
const {data} = await axios.get('api/posts')
setPosts(data)
}
getUsers()
}, [])
Используйте IIFE (функция-выражение, вызываемая сразу после создания) в useEffect.
useEffect(() => {
(async () => {
const {data} = await axios.get('api/posts')
setPosts(data)
})()
}, [])
А вот так, делать не надо!
useEffect( async () => {
const {data} = await axios.get('api/posts')
setPosts(data)
}, [])
Советы и рекомендации по эффективному использованию useEffect
Давайте посмотрим на некоторые приемы, которые мы можем использовать в useEffect.
Используйте UseEffect на верхнем уровне.
if(a > b){
useEffect(() => {
// incorrect usage
}, [])
}
useEffect(() => {
if(a > b){
// incorrect usage
}
}, [])
useEffect(() => {
if(a < b) return
// correct usage
}, [])
Не нужно использовать useEffect в условных выражениях, циклах и вложенных функциях.
Используйте useEffect для одной задачи.
Выполняйте только одну задачу с использованием useEffect. Для нескольких задач вы можете использовать несколько useEffect для одного и того же компонента. Разделите работу на части и назначьте useEffect для каждой. Назначение useEffect на короткие и одноцелевые функции предотвращает нежелательную повторную визуализацию и позволяет сохранить код чистым и читабельным.
Заключение
useEffect — очень полезный и широко используемый хук React, который стоит освоить. Когда вы привыкнете к нему, вам захочется использовать его постоянно.
Делитесь своим опытом в комментариях.
Комментарии (11)
CALLlA
09.09.2022 14:33В первом и втором примере раздела "Использование функций Async-Await в useEffect" не напутал ли автор с getUsers и getPosts ?
Alexandroppolus
09.09.2022 16:59+3С эффектом главное соблюсти меру: https://beta.reactjs.org/learn/you-might-not-need-an-effect
strannik_k
10.09.2022 00:09useEffect(() => { (async () => { const {data} = await axios.get('api/posts') setPosts(data) })() }, [])
Я бы предпочел избавиться от такого количества скобок и писать например так:
useMountEffectAsync(async () => { const {data} = await axios.get('api/posts') setPosts(data) })
faiwer
10.09.2022 01:21Я бы предпочел избавиться от такого количества скобок и писать например так:
Фантазия на заданную тему:
function useMountEffectAsync(fn: (isAborted: () => boolean) => Promise<void>) { useEffect(() => { let isAborted = false; fn(() => isAborted).catch(defaultAppErrorHandler); return () => { isAborted = true; }; }, []); }
Alexandroppolus
10.09.2022 02:17+1isAborted: () => boolean
Тогда уж стандартный AbortController, https://learn.javascript.ru/fetch-abort
faiwer
10.09.2022 17:37> Можно, но зачем? Чтобы abort-ить прямо из fn?
Понял. Чтобы передать в fetch как signal. Да, логично
DmitryKoterov
10.09.2022 11:29побочный эффект в функции обратного вызова
Какой кошмарный канцелярит. Еще бы добавить «определенная путем осуществления передачи вторым значением подпрограммы для ПЭВМ».
khuzhinru
12.09.2022 11:17Когда вы привыкнете к нему, вам захочется использовать его постоянно. Делитесь своим опытом в комментариях.
По своему опыту могу сказать, что желание использовать этот хук постоянно надо убивать на корню. Если компонент чуть сложнее, чем вот эти примеры со счётчиками, то сложность отладки кода с каждым useEffect растет экспоненциально. Особенно когда их зависимости приходят из вне (из родителей/из менеджера состояний/из другого кастомного хука и т.д.). Эти зависимости могут изменятся тоже неявно из какого нибудь внешнего useEffect, что в какой-то момент контролировать это невозможно. А если еще без тестов? А если кто-то вообще забыл или проигнорил предупреждения линтера и не указал все зависимости и построил на этом огромную фичу?
В общем, инструмент хороший, но опасный. По возможности, опасность нужно избегать
Nik_o_lay
Чем плох этот вариант?
rafuck
Более того, в третьем варианте, который correct, присутствует логическая ошибка.
traly_valy
Не, тут логической ошибки нет, имеется ввиду вместо
if (exp) {
do smth
}
Писать
if (!exp) return
do smth
НО, почему так лучше - загадка дыры..