Как с помощью useEffect пропустить применение эффекта при первоначальном рендеринге?

С новыми хуками эффектов React я могу сказать React пропустить применение эффекта, если определенные значения не изменились между повторными рендерингами - пример из документации React:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

Но в приведенном выше примере эффект применяется при первоначальном рендеринге и при последующих повторных рендерингах, когда был изменен count. Как я могу сказать React пропустить эффект при первоначальном рендеринге?

Вы смотрели React.useMemo?

Pankaj Parag 09.07.2020 01:01
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
126
1
103 344
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

Ответ принят как подходящий

Как говорится в руководстве,

The Effect Hook, useEffect, adds the ability to perform side effects from a function component. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes, but unified into a single API.

В этом примере из руководства ожидается, что count равен 0 только при начальном рендеринге:

const [count, setCount] = useState(0);

Так что он будет работать как componentDidUpdate с дополнительной проверкой:

useEffect(() => {
  if (count)
    document.title = `You clicked ${count} times`;
}, [count]);

По сути, вот как может работать кастомный хук, который можно использовать вместо useEffect:

function useDidUpdateEffect(fn, inputs) {
  const didMountRef = useRef(false);

  useEffect(() => {
    if (didMountRef.current)
      return fn();
    else
      didMountRef.current = true;
  }, inputs);
}

Кредиты идут на @Tholle за предложение useRef вместо setState.

Где предложение для useRef вместо setState? Я не вижу этого на этой странице и хотел бы понять, почему.

rob-gordon 24.06.2019 17:09

@ rob-gordon Это был комментарий, который был удален после обновления ответа. Причина в том, что useState приведет к ненужному обновлению компонентов.

Estus Flask 24.06.2019 17:18

Этот подход работает, но нарушает правило линтера react-hooks / excustive-deps. Т.е. fn не указывается в массиве deps. У кого-нибудь есть подход, который не нарушает лучших практик React?

Justin Lang 02.10.2019 00:22

@JustinLang React linter rules! = Лучшие практики, они пытаются решить только общие проблемы с перехватами. Правила ESLint неразумны и могут привести к ложным срабатываниям или отрицательным результатам. Пока логика, лежащая в основе ловушки, верна, правило можно безопасно игнорировать с помощью комментариев eslint-disable или eslint-disable-next. В этом случае fn не должен использоваться в качестве входных данных. См. Объяснение, reactjs.org/docs/…. Если что-то внутри fn вводит deps, это больше похоже на то, что они должны быть предоставлены напрямую как inputs.

Estus Flask 02.10.2019 13:33
Примечание: если вы используете несколько useEffects, которые проверяют didMountRef, убедитесь, что только последний (внизу) устанавливает для didMountRef значение false. Реакция проходит через useEffects по порядку!
Dror Bar 18.04.2021 13:07

Я использую обычную переменную состояния вместо ссылки.

// Initializing didMount as false
const [didMount, setDidMount] = useState(false)

// Setting didMount to true upon mounting
useEffect(() => { setDidMount(true) }, [])

// Now that we have a variable that tells us wether or not the component has
// mounted we can change the behavior of the other effect based on that
const [count, setCount] = useState(0)
useEffect(() => {
  if (didMount) document.title = `You clicked ${count} times`
}, [count])

Мы можем реорганизовать логику didMount как настраиваемую ловушку, подобную этой.

function useDidMount() {
  const [didMount, setDidMount] = useState(false)
  useEffect(() => { setDidMount(true) }, [])

  return didMount
}

Наконец, мы можем использовать его в нашем компоненте вот так.

const didMount = useDidMount()

const [count, setCount] = useState(0)
useEffect(() => {
  if (didMount) document.title = `You clicked ${count} times`
}, [count])

ОБНОВИТЬ Использование хука useRef, чтобы избежать лишнего повторного рендеринга (спасибо @TomEsterez за предложение)

На этот раз наш пользовательский хук возвращает функцию, возвращающую текущее значение нашего ref. Вы тоже можете использовать ссылку напрямую, но мне это больше нравится.

function useDidMount() {
  const mountRef = useRef(false);

  useEffect(() => { mountRef.current = true }, []);

  return () => mountRef.current;
}

использование

const MyComponent = () => {
  const didMount = useDidMount();

  useEffect(() => {
    if (didMount()) // do something
    else // do something else
  })

  return (
    <div>something</div>
  );
}

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

useRef лучше подходит для этого, потому что useState вызовет дополнительную и бесполезную визуализацию компонента: codeandbox.io/embed/youthful-goldberg-pz3cx
Tom Esterez 19.08.2019 19:45

у вас есть ошибка в вашей функции useDidMount. Вы должны заключить mountRef.current = true в useEffect() с фигурными скобками { ...}. Оставить их - все равно что записать return mountRef.current = true, и это вызовет ошибку типа An effect function must not return anything besides a function, which is used for clean-up. You returned: true.

suther 13.11.2020 09:00

Вот настраиваемый хук, который просто предоставляет логический флаг, чтобы указать, является ли текущий рендеринг первым рендером (когда компонент был смонтирован). Это примерно то же самое, что и некоторые другие ответы, но вы можете использовать флаг в useEffect или функции рендеринга или в любом другом месте в нужном вам компоненте. Может, кто-нибудь предложит имя получше.

import { useRef, useEffect } from 'react';

export const useIsMount = () => {
  const isMountRef = useRef(true);
  useEffect(() => {
    isMountRef.current = false;
  }, []);
  return isMountRef.current;
};

Вы можете использовать это как:

import React, { useEffect } from 'react';

import { useIsMount } from './useIsMount';

const MyComponent = () => {
  const isMount = useIsMount();

  useEffect(() => {
    if (isMount) {
      console.info('First Render');
    } else {
      console.info('Subsequent Render');
    }
  });

  return isMount ? <p>First Render</p> : <p>Subsequent Render</p>;
};

А вот и тест, если вам интересно:

import { renderHook } from '@testing-library/react-hooks';

import { useIsMount } from '../useIsMount';

describe('useIsMount', () => {
  it('should be true on first render and false after', () => {
    const { result, rerender } = renderHook(() => useIsMount());
    expect(result.current).toEqual(true);
    rerender();
    expect(result.current).toEqual(false);
    rerender();
    expect(result.current).toEqual(false);
  });
});

Нашим вариантом использования было скрытие анимированных элементов, если исходные свойства указывают, что они должны быть скрыты. На более поздних рендерах, если реквизит изменился, мы хотели, чтобы элементы анимировались.

Импорт изменен с react-hooks-testing-library на @testing-library/react-hooksgithub.com/testing-library/react-hooks-testing-library/relea‌ ses /…

Scotty Waggoner 16.07.2019 01:28

Почему вы выбрали isMount, а не didMount или isMounted?

vsync 07.11.2019 11:33

Думаю, я думал о том, является ли текущий рендер рендером того места, где происходило монтирование. Я согласен, это звучит немного странно, возвращаясь к этому сейчас. Но это верно при первом рендеринге и ложно после, поэтому ваши предложения могут вводить в заблуждение. isFirstRender может работать.

Scotty Waggoner 07.11.2019 18:29

Спасибо за крючок! Я согласен с @ScottyWaggoner, isFirstRender - лучшее имя

abumalick 11.04.2020 14:12

Спасибо за Ваш ответ. Я использую ваш крючок так же, как первое значение false, после установки true и переименования didMount :)

Tugrul Emre Atalay 30.05.2020 17:49

Добавление теста - ОГОНЬ! Отличный ответ.

wlh 29.01.2021 21:14

Спасибо вам большое за это ! Я полностью согласен с названием. isFirstRender имеет смысл. Я уже использовал его в своем проекте с таким названием =)

Enes 07.10.2021 01:24

Потрясающий материал !!

Alex Dunlop 25.11.2021 03:53

Я нашел более простое решение, в котором нет необходимости использовать другой крючок, но у него есть недостатки.

useEffect(() => {
  // skip initial render
  return () => {
    // do something with dependency
  }
}, [dependency])

Это просто пример того, что есть другие способы сделать это, если ваш случай очень прост.

Недостатком этого является то, что вы не можете получить эффект очистки и будет выполняться только тогда, когда массив зависимостей изменится во второй раз.

Это не рекомендуется использовать, и вы должны использовать то, что говорят другие ответы, но я добавил это только здесь, чтобы люди знали, что есть несколько способов сделать это.

Редактировать:

Чтобы было понятнее, подход ты не должен использовать это к решению проблемы в вопросе (пропуск начального рендеринга) предназначен только для обучающих целей, которые показывают, что вы можете делать то же самое по-разному. Если вам нужно пропустить начальную визуализацию, используйте этот подход для других ответов.

Я только что кое-что узнал. Я не думал, что это сработает, потом попробовал, и это действительно так. Спасибо!

zimmerbimmer 11.04.2020 23:30

React должен предоставить лучший способ для этого, но этот вопрос открыт на Github, и предлагается написать собственное решение самостоятельно (что совершенно бессмысленно).

html_programmer 12.07.2020 13:18

Проблема с этим в том, что он также будет выполняться при размонтировании, что не идеально.

Nick Alves 21.07.2020 23:16

@ncesar, если это был единственный ответ, который сработал, возможно, вы делаете что-то не так, потому что другой ответ правильный, а мой предназначен только для обучения целям

Vencovsky 04.12.2020 12:43

Дружественный хук TypeScript и CRA, замените его на useEffect, этот хук работает как useEffect, но не срабатывает, пока происходит первый рендеринг.

import * as React from 'react'

export const useLazyEffect:typeof React.useEffect = (cb, dep) => {
  const initializeRef = React.useRef<boolean>(false)

  React.useEffect((...args) => {
    if (initializeRef.current) {
      cb(...args)
    } else {
      initializeRef.current = true
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, dep)
}

Нижеприведенное решение похоже на приведенное выше, но я предпочитаю немного более чистый способ.

const [isMount, setIsMount] = useState(true);
useEffect(()=>{
        if (isMount){
            setIsMount(false);
            return;
        }
        
        //Do anything here for 2nd render onwards
}, [args])

Это не идеальный способ сделать это, поскольку установка состояния вызывает другой рендеринг.

Daniel 31.01.2021 02:18

Вот моя реализация, основанная на отвечать Estus Flask, написанном на Typescript. Он также поддерживает обратный вызов очистки.

import { DependencyList, EffectCallback, useEffect, useRef } from 'react';

export function useDidUpdateEffect(
  effect: EffectCallback,
  deps?: DependencyList
) {
  // a flag to check if the component did mount (first render's passed)
  // it's unrelated to the rendering process so we don't useState here
  const didMountRef = useRef(false);

  // effect callback runs when the dependency array changes, it also runs
  // after the component mounted for the first time.
  useEffect(() => {
    // if so, mark the component as mounted and skip the first effect call
    if (!didMountRef.current) {
      didMountRef.current = true;
    } else {
      // subsequent useEffect callback invocations will execute the effect as normal
      return effect();
    }
  }, deps);
}

Живая демонстрация

Живая демонстрация ниже демонстрирует разницу между хуками useEffect и useDidUpdateEffect.

Edit 53179075/with-useeffect-how-can-i-skip-applying-an-effect-upon-the-initial-render

Я собирался прокомментировать принятый на данный момент ответ, но не хватило места!

Во-первых, при использовании функциональных компонентов важно отказаться от мышления в терминах событий жизненного цикла. Подумайте об изменениях свойств / состояний. У меня была аналогичная ситуация, когда я хотел, чтобы срабатывала только конкретная функция useEffect, когда конкретная опора (parentValue в моем случае) меняет свое начальное состояние. Итак, я создал реф, основанный на его начальном значении:

const parentValueRef = useRef(parentValue);

а затем включил следующее в начало useEffect fn:

if (parentValue === parentValueRef.current) return;
parentValueRef.current = parentValue;

(Как правило, не запускайте эффект, если parentValue не изменился. Обновите ссылку, если она изменилась, готовность к следующей проверке, и продолжайте запускать эффект)

Таким образом, хотя другие предлагаемые решения решат конкретный вариант использования, который вы указали, в конечном итоге они помогут изменить то, как вы думаете о функциональных компонентах.

Думайте о них как о рендеринге компонента на основе некоторых свойств.

Если вам действительно нужно какое-то локальное состояние, то useState предоставит это, но не предполагать, ваша проблема будет решена путем сохранения локального состояния.

Если у вас есть код, который изменит ваши реквизиты во время рендеринга, этот `` побочный эффект '' необходимо обернуть в useEffect, но его цель - получить чистый рендер, на который не влияют какие-либо изменения во время рендеринга. . Хук useEffect будет запущен после завершения рендеринга и, как вы указали, он будет запускаться при каждом рендеринге - если только второй параметр не используется для предоставления списка свойств / состояний, чтобы определить, какие измененные элементы приведут к его изменению. выполнить последующие разы.

Удачи на пути к функциональным компонентам / крючкам! Иногда необходимо отучиться чему-то, чтобы освоить новый способ ведения дел :) Это отличный праймер: https://overrected.io/a-complete-guide-to-useeffect/

Разрешите представить вам реагировать-использовать.

Хочу бежать:

только после первого рендера? useUpdateEffect

только один раз? useEffectOnce

это первое крепление?? useFirstMountState

Хотите запустить эффект с глубокое сравнение, поверхностное сравнение или дроссель? Намного больше здесь.

Не хотите устанавливать библиотеку? Проверьте код и скопируйте. (может быть, star для хороших ребят тоже)

Другие вопросы по теме