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

Согласно документам:

componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.

Мы можем использовать новый хук useEffect() для имитации componentDidUpdate(), но похоже, что useEffect() запускается после каждого рендеринга, даже в первый раз. Как мне заставить его не запускаться при первоначальном рендеринге?

Как вы можете видеть в примере ниже, componentDidUpdateFunction печатается во время первоначального рендеринга, но componentDidUpdateClass не печатается во время первоначального рендеринга.

function ComponentDidUpdateFunction() {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    console.info("componentDidUpdateFunction");
  });

  return (
    <div>
      <p>componentDidUpdateFunction: {count} times</p>
      <button
        onClick = {() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

class ComponentDidUpdateClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  componentDidUpdate() {
    console.info("componentDidUpdateClass");
  }

  render() {
    return (
      <div>
        <p>componentDidUpdateClass: {this.state.count} times</p>
        <button
          onClick = {() => {
            this.setState({ count: this.state.count + 1 });
          }}
        >
          Click Me
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <div>
    <ComponentDidUpdateFunction />
    <ComponentDidUpdateClass />
  </div>,
  document.querySelector("#app")
);
<script src = "https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id = "app"></div>

Могу я спросить, каков вариант использования, когда имеет смысл делать что-то на основе количества визуализаций, а не явной переменной состояния, такой как count?

Aprillion 15.11.2018 16:20

@Aprillion, в моем случае измените содержимое H2, у которого есть текст, который необходимо изменить после списка элементов, пуст и даже был другим в начале. Тот же список также пуст в начале перед выборкой данных из API, поэтому при нормальном условном рендеринге на основе длины массива начальное значение переопределяется

Carmine Tambascia 05.11.2021 15:32
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
194
2
126 486
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

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

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

Если мы хотим, чтобы эффект выполнялся в той же фазе, что и componentDidUpdate, мы можем вместо этого использовать useLayoutEffect.

Пример

const { useState, useRef, useLayoutEffect } = React;

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

  const firstUpdate = useRef(true);
  useLayoutEffect(() => {
    if (firstUpdate.current) {
      firstUpdate.current = false;
      return;
    }

    console.info("componentDidUpdateFunction");
  });

  return (
    <div>
      <p>componentDidUpdateFunction: {count} times</p>
      <button
        onClick = {() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

ReactDOM.render(
  <ComponentDidUpdateFunction />,
  document.getElementById("app")
);
<script src = "https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id = "app"></div>

Я попытался заменить useRef на useState, но использование установщика вызвало повторный рендеринг, чего не происходит при назначении на firstUpdate.current, поэтому я думаю, что это единственный хороший способ :)

Aprillion 15.11.2018 16:16

Может ли кто-нибудь объяснить, зачем использовать эффект макета, если мы не изменяем и не измеряем DOM?

ZenVentzi 06.03.2019 13:45

@ZenVentzi В этом примере это не обязательно, но вопрос был в том, как имитировать componentDidUpdate с помощью хуков, поэтому я его и использовал.

Tholle 06.03.2019 13:52

Я создал собственный хук здесь на основе этого ответа. Спасибо за реализацию!

Patrick Roberts 05.11.2019 21:09

Вы можете превратить его в нестандартные хуки, вот так:

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        if (didMount.current) func();
        else didMount.current = true;
    }, deps);
}

export default useDidMountEffect;

Пример использования:

import React, { useState, useEffect } from 'react';

import useDidMountEffect from '../path/to/useDidMountEffect';

const MyComponent = (props) => {    
    const [state, setState] = useState({
        key: false
    });    

    useEffect(() => {
        // you know what is this, don't you?
    }, []);

    useDidMountEffect(() => {
        // react please run me if 'key' changes, but not on initial render
    }, [state.key]);    

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

Этот подход вызывает предупреждения о том, что список зависимостей не является литералом массива.

theprogrammer 26.10.2019 07:57

Я использую этот хук в своих проектах, и я не видел никаких предупреждений, не могли бы вы предоставить дополнительную информацию?

Mehdi Dehghani 04.05.2020 17:50

@vsync Вы думаете о другом случае, когда хотите запустить эффект один раз при первоначальном рендеринге и никогда больше

Programming Guy 21.05.2020 05:13

@vsync В разделе примечаний reactjs.org/docs/… специально сказано: «Если вы хотите запустить эффект и очистить его только один раз (при монтировании и размонтировании), вы можете передать пустой массив ([]) в качестве второго аргумента». Это соответствует наблюдаемому мной поведению.

Programming Guy 22.05.2020 11:58

Я также получаю предупреждения о том, что список зависимостей не является литералом массива и отсутствует зависимость: 'func'. Правило линтера, отмеченное для обоих, - это react-hooks / excustive-deps.

plong0 23.02.2021 22:36

@ plong0 Хорошо, я протестировал упомянутый код (в своем ответе), и я не получил никакого предупреждения, я думаю, что это связано с версией реакции, которую вы используете, или вашей зависимостью (state.key в моем ответе), поэтому мне нужно больше информации в чтобы дать вам лучший ответ.

Mehdi Dehghani 24.02.2021 06:22

Нет необходимости импортировать React в файл хуков

gogaz 03.06.2021 12:33

он забыл добавить: [] вокруг deps ... должно быть что-то вроде [deps] ... но у меня это не работает

Carlos Eduardo Olivera Filho 07.09.2021 23:59

@CarlosEduardoOliveraFilho Я не забыл, это не так. когда вы используете этот хук, вы должны передавать зависимости в виде массива, как и сам useEffect. напишите свой код с помощью useEffect, если все работает, просто замените useEffect на useDidMountEffect, и все готово.

Mehdi Dehghani 08.09.2021 05:22

@Mehdi о, хорошо, вы правы, извините, deps, полученные в качестве параметра вашего пользовательского хука, уже являются массивом, поэтому вам не нужен []. Спасибо!

Carlos Eduardo Olivera Filho 09.09.2021 09:35

@MehdiDehghani, ваше решение работает отлично, одно дополнение, которое вам нужно сделать, это размонтировать, сбросив значение didMount.current на false. Когда пытаться использовать этот настраиваемый хук где-нибудь еще, вы не получите значение кеша.

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        let unmount;
        if (didMount.current) unmount = func();
        else didMount.current = true;

        return () => {
            didMount.current = false;
            unmount && unmount();
        }
    }, deps);
}

export default useDidMountEffect;

Я не уверен, что это необходимо, поскольку, если компонент все-таки размонтируется, потому что, если он будет перемонтирован, didMount уже будет повторно инициализирован на false.

Cameron Yick 29.04.2020 16:37

@CameronYick это необходимо, потому что при использовании Fast Refresh функция очистки будет работать, но значение ref не будет очищено. Это приводит к сбоям во время разработки - но в продакшене это нормально.

andreialecu 13.04.2021 09:49

@ravi, ваш не вызывает переданную функцию размонтирования. Вот версия, которая немного более полная:

/**
 * Identical to React.useEffect, except that it never runs on mount. This is
 * the equivalent of the componentDidUpdate lifecycle function.
 *
 * @param {function:function} effect - A useEffect effect.
 * @param {array} [dependencies] - useEffect dependency list.
 */
export const useEffectExceptOnMount = (effect, dependencies) => {
  const mounted = React.useRef(false);
  React.useEffect(() => {
    if (mounted.current) {
      const unmount = effect();
      return () => unmount && unmount();
    } else {
      mounted.current = true;
    }
  }, dependencies);

  // Reset on unmount for the next mount.
  React.useEffect(() => {
    return () => mounted.current = false;
  }, []);
};

Привет @Whatabrain, как использовать этот настраиваемый перехватчик для передачи списка независимостей? Не пустой, который будет таким же, как componentDidmount, а что-то вроде useEffect(() => {...});

Kev D. 04.08.2020 04:20

@KevDing Должно быть так же просто, как опустить параметр dependencies при его вызове.

Whatabrain 05.08.2020 17:27

Я сделал простой перехватчик useFirstRender для обработки таких случаев, как фокусирование ввода формы:

import { useRef, useEffect } from 'react';

export function useFirstRender() {
  const firstRender = useRef(true);

  useEffect(() => {
    firstRender.current = false;
  }, []);

  return firstRender.current;
}

Он начинается как true, затем переключается на false в useEffect, который запускается только один раз и никогда больше.

В своем компоненте используйте его:

const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);

useEffect(() => {
  if (firstRender || errors.phoneNumber) {
    phoneNumberRef.current.focus();
  }
}, [firstRender, errors.phoneNumber]);

В вашем случае просто используйте if (!firstRender) { ....

Это лучшая реализация, которую я когда-либо создавал с использованием typescript. По сути, идея та же самая, с использованием Ref, но я также рассматриваю обратный вызов, возвращаемый useEffect, для выполнения очистки при размонтировании компонента.

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

/**
 * @param effect 
 * @param dependencies
 *  
 */
export default function useNoInitialEffect(
  effect: EffectCallback,
  dependencies?: DependencyList
) {
  //Preserving the true by default as initial render cycle
  const initialRender = useRef(true);

  useEffect(() => {
    let effectReturns: void | (() => void) = () => {};

    // Updating the ref to false on the first render, causing
    // subsequent render to execute the effect
    if (initialRender.current) {
      initialRender.current = false;
    } else {
      effectReturns = effect();
    }

    // Preserving and allowing the Destructor returned by the effect
    // to execute on component unmount and perform cleanup if
    // required.
    if (effectReturns && typeof effectReturns === 'function') {
      return effectReturns;
    } 
    return undefined;
  }, dependencies);
}

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

useuseNoInitialEffect(() => {
  // perform something, returning callback is supported
}, [a, b]);

Тот же подход, что и Толле ответ, но с использованием useState вместо useRef.

const [skipCount, setSkipCount] = useState(true);

...

useEffect(() => {
    if (skipCount) setSkipCount(false);
    if (!skipCount) runYourFunction();
}, [dependencies])

Это был хороший вариант, который позволил мне найти свое решение

Carmine Tambascia 05.11.2021 16:01

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

const [firstRenderDone, setFirstRenderDone] = useState(false);

//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
  setFirstRenderDone(true);
}, []);

// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
  if (firstRenderDone){
    console.info("componentDidUpdateFunction");
  }
}, [firstRenderDone]);

Все предыдущие хороши, но это может быть достигнуто более простым способом, учитывая, что действие в useEffect можно «пропустить», поместив условие if (или любое другое), которое по сути не запускается в первый раз, но все еще с зависимостью.

Например, у меня был случай:

  1. Загружать данные из API, но мой заголовок должен быть «Загрузка» до тех пор, пока не указана дата, поэтому у меня есть массив, туры, которые вначале пусты и показывают текст «Отображение»
  2. Сделайте визуализацию компонента с другой информацией из этого API.
  3. Пользователь может удалить одну за другой эту информацию, даже если массив тура снова станет пустым в начале, но на этот раз выборка API уже выполнена.
  4. Как только список тура станет пустым после удаления, покажите другое название.

поэтому моим «решением» было создание другого useState для создания логического значения, которое изменится только после того, как выборка данных сделает другое условие в useEffect истинным, чтобы запустить другую функцию, которая также зависит от продолжительности обхода.

useEffect(() => {
  if (isTitle) {
    changeTitle(newTitle)
  }else{
    isSetTitle(true)
  }
}, [tours])

вот мой App.js

import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'

const url = 'API url'

let newTours

function App() {
  const [loading, setLoading ] = useState(true)
  const [tours, setTours] = useState([])
  const [isTitle, isSetTitle] = useState(false)
  const [title, setTitle] = useState("Our Tours")

  const newTitle = "Tours are empty"

  const removeTours = (id) => {
    newTours = tours.filter(tour => ( tour.id !== id))

    return setTours(newTours)
  }

  const changeTitle = (title) =>{
    if (tours.length === 0 && loading === false){
      setTitle(title)
    }
  }

const fetchTours = async () => {
  setLoading(true)

  try {
    const response = await fetch(url)
    const tours = await response.json()
    setLoading(false)
    setTours(tours)
  }catch(error) {
    setLoading(false)
    console.info(error)
  }  
}


useEffect(()=>{
  fetchTours()
},[])

useEffect(() => {
  if (isTitle) {
    changeTitle(newTitle)
  }else{
    isSetTitle(true)
  }
}, [tours])


if (loading){
  return (
    <main>
      <Loading />
    </main>
  )  
}else{
  return ( 

    <main>
      <Tours tours = {tours} title = {title} changeTitle = {changeTitle}           
removeTours = {removeTours} />
    </main>
  )  
 }
}



export default App

Я не вижу разницы в этом подходе, за исключением порядка условий в инструкции if.

Luigi 06.11.2021 17:17

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