В этой статье рассмотрим советы и приёмы, которые помогут более профессионально написать код на 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 могут получить бесконечный цикл. В этом разделе будут описаны распространенные сценарии, которые генерируют бесконечные циклы, и то, как их избежать.

  1. Если не указан массив зависимостей:

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 
}
  1. Если в массиве зависимостей указана функция:

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?

  1. Создайте асинхронную функцию вне useEffect и вызовите ее в useEffect.

const getPosts = async () => {
 const {data} = await axios.get('api/posts')
 setPosts(data)
}
useEffect(() => {
 getUsers()
}, [])
  1. Создайте асинхронную функцию в useEffect и вызовите ее в useEffect.

useEffect(() => {
const getPosts = async () => {
 const {data} = await axios.get('api/posts')
 setPosts(data)
}
 getUsers()
}, [])
  1. Используйте 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.

  1. Используйте UseEffect на верхнем уровне.

if(a > b){ 
   useEffect(() => {
     // incorrect usage
   }, [])
}
useEffect(() => {
   if(a > b){ 
     // incorrect usage
   }
}, [])
useEffect(() => {
   if(a < b) return
     // correct usage
}, [])

Не нужно использовать useEffect в условных выражениях, циклах и вложенных функциях.

  1. Используйте useEffect для одной задачи.

Выполняйте только одну задачу с использованием useEffect. Для нескольких задач вы можете использовать несколько useEffect для одного и того же компонента. Разделите работу на части и назначьте useEffect для каждой. Назначение useEffect на короткие и одноцелевые функции предотвращает нежелательную повторную визуализацию и позволяет сохранить код чистым и читабельным.

Заключение

useEffect — очень полезный и широко используемый хук React, который стоит освоить. Когда вы привыкнете к нему, вам захочется использовать его постоянно.

Делитесь своим опытом в комментариях.

Комментарии (11)


  1. Nik_o_lay
    09.09.2022 14:26
    +4

    Чем плох этот вариант?

    useEffect(() => {
       if(a > b){ 
         // incorrect usage
       }
    }, [])


    1. rafuck
      09.09.2022 21:46

      Более того, в третьем варианте, который correct, присутствует логическая ошибка.


      1. traly_valy
        12.09.2022 11:35

        Не, тут логической ошибки нет, имеется ввиду вместо

        if (exp) {
        do smth
        }


        Писать

        if (!exp) return
        do smth



        НО, почему так лучше - загадка дыры..


  1. CALLlA
    09.09.2022 14:33

    В первом и втором примере раздела "Использование функций Async-Await в useEffect" не напутал ли автор с getUsers и getPosts ?


  1. Alexandroppolus
    09.09.2022 16:59
    +3

    С эффектом главное соблюсти меру: https://beta.reactjs.org/learn/you-might-not-need-an-effect


  1. strannik_k
    10.09.2022 00:09

    useEffect(() => {
      (async () => {
        const {data} = await axios.get('api/posts')
        setPosts(data)
      })()
    }, [])

    Я бы предпочел избавиться от такого количества скобок и писать например так:

    useMountEffectAsync(async () => {
      const {data} = await axios.get('api/posts')
      setPosts(data)
    })


    1. faiwer
      10.09.2022 01:21

      Я бы предпочел избавиться от такого количества скобок и писать например так:

      Фантазия на заданную тему:


      function useMountEffectAsync(fn: (isAborted: () => boolean) => Promise<void>) {
          useEffect(() => {
              let isAborted = false;
              fn(() => isAborted).catch(defaultAppErrorHandler);
              return () => { isAborted = true; };
          }, []);
      }


      1. Alexandroppolus
        10.09.2022 02:17
        +1

        isAborted: () => boolean

        Тогда уж стандартный AbortController, https://learn.javascript.ru/fetch-abort


        1. faiwer
          10.09.2022 17:37

          > Можно, но зачем? Чтобы abort-ить прямо из fn?
          Понял. Чтобы передать в fetch как signal. Да, логично


  1. DmitryKoterov
    10.09.2022 11:29

    побочный эффект в функции обратного вызова

    Какой кошмарный канцелярит. Еще бы добавить «определенная путем осуществления передачи вторым значением подпрограммы для ПЭВМ».


  1. khuzhinru
    12.09.2022 11:17

    Когда вы привыкнете к нему, вам захочется использовать его постоянно. Делитесь своим опытом в комментариях.

    По своему опыту могу сказать, что желание использовать этот хук постоянно надо убивать на корню. Если компонент чуть сложнее, чем вот эти примеры со счётчиками, то сложность отладки кода с каждым useEffect растет экспоненциально. Особенно когда их зависимости приходят из вне (из родителей/из менеджера состояний/из другого кастомного хука и т.д.). Эти зависимости могут изменятся тоже неявно из какого нибудь внешнего useEffect, что в какой-то момент контролировать это невозможно. А если еще без тестов? А если кто-то вообще забыл или проигнорил предупреждения линтера и не указал все зависимости и построил на этом огромную фичу?

    В общем, инструмент хороший, но опасный. По возможности, опасность нужно избегать