Как очистить setTimeout/setInterval в обработчике событий в React?

#javascript #reactjs

Вопрос:

Как я могу очистить функцию, подобную setTimeout или setInterval в обработчике событий в React? Или в этом нет необходимости?

 import React from 'react'

function App(){
  return (
    <button onClick={() => {
      setTimeout(() => {
        console.log('you have clicked me')
        //How to clean this up?
      }, 500)
    }}>Click me</button>
  )
}

export default App
 

Ответ №1:

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

Чтобы сделать это в функциональном компоненте, подобном вашему, вы используете useEffect функцию очистки с пустым массивом зависимостей. Вероятно, вы хотите сохранить дескриптор таймера в a ref .

(FWIW, я бы также определил функцию вне onClick атрибута, просто для ясности.)

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

function App() {
    const instance = useRef({timer: 0});

    useEffect(() => {
        // What you return is the cleanup function
        return () => {
            clearTimeout(instance.current.timer);
        };
    }, []);

    const onClick = () => {
        // Clear any previous one (it's fine if it's `0`,
        // `clearTimeout` won't do anything)
        clearTimeout(instance.current.timer);
        // Set the timeout and remember the value on the object
        instance.current.timer = setTimeout(() => {
            console.log('you have clicked me')
            //How to clean this up?
        }, 500);
    };

    return (
        <button onClick={onClick}>Click me</button>
    )
}

export default App;
 

Объект, который вы храните в качестве ссылки, обычно является полезным местом для размещения вещей, которые вы в противном случае поместили this бы в компонент класса.

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

Комментарии:

1. Привет, Т. Джей, есть ли преимущества хранения идентификатора таймера в качестве ссылки вместо обычной переменной?

2. @AmirSaleem — Вы имеете в виду локальную переменную в функции? Да, это не будет работать надежно. 🙂 Функция вызывается в любое время, когда React необходимо повторно отобразить компонент, и локальная переменная воссоздается каждый раз при вызове функции, в то время как экземпляр from useRef стабилен. В локальном случае, если компонент был повторно отрисован хотя бы один раз до нажатия кнопки, useEffect обратный вызов завершится над устаревшей копией переменной, а не той, которую устанавливает кнопка. (Поскольку useEffect из-за пустого массива зависимостей используется только первая версия обратного вызова.)

3. (Ой, в ответе выше была опечатка. Если вы видите instance.timer , а не instance.current.timer , нажмите обновить.)

4. Спасибо Ти Джею за ваше подробное объяснение. Это было проницательно

Ответ №2:

Еще одно решение (Живая демонстрация):

 import React, { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";

export default function TestComponent(props) {
  const [text, setText] = useState("");

  const click = useAsyncCallback(function* (ms) {
    yield CPromise.delay(ms);
    setText("done!"   new Date().toLocaleTimeString());
  }, []);

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>{text}</div>
      <button onClick={() => click(2000)}>Click me!</button>
      <button onClick={click.cancel}>Cancel scheduled task</button>
    </div>
  );
}
 

В случае, если вы хотите отменить предыдущую отложенную задачу (демонстрационная версия):

 import React, { useState } from "react";
import { useAsyncCallback } from "use-async-effect2";
import { CPromise } from "c-promise2";

export default function TestComponent(props) {
  const [text, setText] = useState("");

  const click = useAsyncCallback(
    function* (ms) {
      console.log("click");
      yield CPromise.delay(ms);
      setText("done!"   new Date().toLocaleTimeString());
    },
    { deps: [], cancelPrevios: true }
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>{text}</div>
      <button onClick={() => click(5000)}>Click me!</button>
      <button onClick={click.cancel}>Cancel scheduled task</button>
    </div>
  );
}
 

Ответ №3:

Снимите таймер при отключении компонента

 import React from 'react'

function App(){
  const timerRef = React.useRef(null)
  React.useEffect(() => {
    return () => {
     // clean
      timerRef.target amp;amp; clearTimeout(timerRef.target)
    }
  },[])
  return (
    <button onClick={() => {
      timerRef.target = setTimeout(() => {
        console.log('you have clicked me')
      }, 500)
    }}>Click me</button>
  )
}

export default App
 

Комментарии:

1.Две проблемы с изложенным: 1. Каждый экземпляр App будет делиться же timer переменной, которая является большой нет-нет. 🙂 2. Ты упускаешь пустой массив зависимостей на useEffect звонок, поэтому он будет вызывать функцию очистки, что каждый раз, когда компонент будет повторно визуализироваться, а не только тогда, когда компонент размонтируется.

2. Спасибо за ваш комментарий, мой плохой, я все исправил.

3. .target должно быть .current в вашей последней версии.