Зробіть React useEffect гачок не запущений на початковому відтворенні


95

Відповідно до документів:

componentDidUpdate()викликається відразу після оновлення. Цей метод не викликається для початкового візуалізації.

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

Як ви можете бачити в прикладі нижче, componentDidUpdateFunctionдрукується під час початкового візуалізації, але componentDidUpdateClassне друкувався під час початкового візуалізації.

function ComponentDidUpdateFunction() {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    console.log("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.log("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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

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


1
Чи можу я запитати, який варіант використання, коли має сенс робити щось на основі кількості рендерів, а не явної змінної стану, як count?
Квітень

Відповіді:


111

Ми можемо використовувати 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.log("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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

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


5
Я намагався замінити useRefна useState, але використання сеттера викликало рендеринг, що не відбувається при призначенні, firstUpdate.currentтому, мабуть, це єдиний приємний спосіб :)
Квітень

2
Хтось може пояснити, навіщо використовувати ефект компонування, якщо ми не мутуємо або не вимірюємо DOM?
ZenVentzi

5
@ZenVentzi Це не потрібно в цьому прикладі, але питання полягало в тому, як імітувати за componentDidUpdateдопомогою гачків, тому я його і використав.
Tholle

Я створив користувальницький гак тут на основі цієї відповіді. Дякуємо за реалізацію!
Патрік Робертс,

57

Ви можете перетворити його на власні гачки , наприклад:

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>
    );
}
// ...

2
Цей підхід видає попередження про те, що список залежностей не є літералом масиву.
theprogrammer

1
Я використовую цей хук у своїх проектах, і я не бачив жодного попередження. Чи можете ви надати більше інформації?
Мехді Деггані

1
@vsync Ви думаєте про інший випадок, коли ви хочете запустити ефект один раз на початковий рендер і ніколи більше
Програмування Гай

2
@vsync У розділі приміток Reactjs.org / docs / ... там конкретно сказано: "Якщо ви хочете запустити ефект і очистити його лише один раз (при монтуванні та демонтажі), ви можете передати порожній масив ([]) як другий аргумент ". Це відповідає спостережуваній поведінці для мене.
Хлопець із програмування

5

Я зробив простий 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) { ....


3

@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, як використовувати цей спеціальний хук при передачі списку, що не залежить від залежності? Не порожній, який би useEffect(() => {...});
збігався з

1
@KevDing Має бути таким простим, як опускання dependenciesпараметра під час його виклику.
Whatabrain

0

@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;

1
Я не впевнений, що це необхідно, оскільки, якщо компонент в кінцевому підсумку демонтується, тому що якщо він перемонтується, didMount буде вже повторно ініціалізовано false.
Камерон Йік,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.