Перехватчики React - правильный способ очистить таймауты и интервалы

Я не понимаю, почему, когда я использую функцию setTimeout, мой компонент реакции начинает бесконечный console.info. Все работает, но ПК начинает чертовски лагать. Некоторые люди говорят, что эта функция по таймауту меняет мое состояние, и этот компонент перерисовки, который устанавливает новый таймер и так далее. Теперь мне нужно понять, как это правильно очистить.

export default function Loading() {
  // if data fetching is slow, after 1 sec i will show some loading animation
  const [showLoading, setShowLoading] = useState(true)
  let timer1 = setTimeout(() => setShowLoading(true), 1000)

  console.info('this message will render  every second')
  return 1
}

Очистить в другой версии кода не помогает:

const [showLoading, setShowLoading] = useState(true)
  let timer1 = setTimeout(() => setShowLoading(true), 1000)
  useEffect(
    () => {
      return () => {
        clearTimeout(timer1)
      }
    },
    [showLoading]
  )

Можете поделиться кодом useState и setShowLoading

Hemadri Dasari 31.10.2018 20:12

@ Think-Twice useState - это предлагаемое обновление для API ReactJS

Mark C. 31.10.2018 20:14

@MarkC. Спасибо, я не знал об этом, так как в настоящее время не работаю над реакцией. Я думаю, что OP должен использовать setTimeout, чем использовать setInterval для отображения загрузчика

Hemadri Dasari 31.10.2018 20:16

я смог сократить свой код.

ZiiMakc 31.10.2018 20:24

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

Hemadri Dasari 31.10.2018 20:26

@ Think-Twice та же проблема. Некоторые парни грустят, что состояние обновления таймера и этот компонент повторно рендерится, а затем снова. Теперь думаю, что делать, чтобы этого не допустить: /

ZiiMakc 31.10.2018 20:28

очистить таймер, когда компонент отключается, например clearTimer (timer1);

Hemadri Dasari 31.10.2018 20:30

@ Think-Twice у меня получилось вот так, та же проблема: const [showLoading, setShowLoading] = useState (true) let timer1 = setTimeout (() => setShowLoading (true), 1000) useEffect (() => {return () => {clearTimeout (timer1)}}, [showLoading])

ZiiMakc 31.10.2018 20:33
Поведение ключевого слова "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) для оценки ваших знаний,...
149
8
189 888
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

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

Определенная функция return () => { /*code/* } внутри useEffect запускается каждый раз при запуске useEffect (кроме первого рендеринга при монтировании компонента) и при размонтировании компонента (если вы больше не отображаете компонент).

Это рабочий способ использования и очистки тайм-аутов или интервалов:

Пример песочницы.

import { useState, useEffect } from "react";

const delay = 5;

export default function App() {
  const [show, setShow] = useState(false);

  useEffect(
    () => {
      let timer1 = setTimeout(() => setShow(true), delay * 1000);

      // this will clear Timeout
      // when component unmount like in willComponentUnmount
      // and show will not change to true
      return () => {
        clearTimeout(timer1);
      };
    },
    // useEffect will run only one time with empty []
    // if you pass a value to array,
    // like this - [data]
    // than clearTimeout will run every time
    // this value changes (useEffect re-run)
    []
  );

  return show ? (
    <div>show is true, {delay}seconds passed</div>
  ) : (
    <div>show is false, wait {delay}seconds</div>
  );
}

Если вам нужно очистить таймауты или интервалы в другом компоненте:

Пример песочницы.

import { useState, useEffect, useRef } from "react";

const delay = 1;

export default function App() {
  const [counter, setCounter] = useState(0);
  const timer = useRef(null); // we can save timer in useRef and pass it to child

  useEffect(() => {
    // useRef value stored in .current property
    timer.current = setInterval(() => setCounter((v) => v + 1), delay * 1000);

    // clear on component unmount
    return () => {
      clearInterval(timer.current);
    };
  }, []);

  return (
    <div>
      <div>Interval is working, counter is: {counter}</div>
      <Child counter = {counter} currentTimer = {timer.current} />
    </div>
  );
}

function Child({ counter, currentTimer }) {
  // this will clearInterval in parent component after counter gets to 5
  useEffect(() => {
    if (counter < 5) return;

    clearInterval(currentTimer);
  }, [counter, currentTimer]);

  return null;
}

Статья Дэна Абрамова.

Что делать, если вам нужно сбросить таймер как «при размонтировании», так и при изменении какого-либо состояния? Вы бы настроили два перехватчика: один с пустым массивом, а другой с соответствующей переменной состояния?

loopmode 18.02.2019 13:34

@loopmode, я думаю, вы можете просто добавить clearTimeout (timer1) в код, где изменяется состояние, но тогда вам нужно будет сохранить свой timer1 в переменной useState.

ZiiMakc 18.02.2019 16:58

Нет ли риска состояния гонки? Я всегда проверяю, был ли вызван return in useEffect на всякий случай, прежде чем пытаться установить переменную состояния.

Jón Trausti Arason 04.03.2019 10:26

@raRaRar возвращает вызов при отключении компонента, о каком условии вы говорите?

ZiiMakc 04.03.2019 11:49

Это было полезно, как и этот пост от самого Дэна Абрамова здесь overrected.io/making-setinterval-declarative-with-react-hoo‌ ks, связанный с stackoverflow.com/a/59274757/470749 А вот версия useInterval на TypeScript: gist.github.com/Danziger/…

Ryan 29.09.2020 19:19

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

Вот рабочий способ установить таймер при монтировании и сбросить его при размонтировании:

function App() {
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      console.info('1 second has passed');
    }, 1000);
    return () => { // Return callback to run on unmount.
      window.clearInterval(timer);
    };
  }, []); // Pass in empty array to run useEffect only on mount.

  return (
    <div>
      Timer Example
    </div>
  );
}

ReactDOM.render(
  <div>
    <App />
  </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>

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

mystrdat 16.05.2019 14:20

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

Итак, как вы уже выяснили, способ использовать setTimeout или setInterval с перехватчиками - это обернуть их в useEffect, например:

React.useEffect(() => {
    const timeoutID = window.setTimeout(() => {
        ...
    }, 1000);

    return () => window.clearTimeout(timeoutID );
}, []);

Как и deps = [], обратный вызов useEffect будет вызываться только один раз. Затем обратный вызов, который вы вернете, будет вызван при размонтировании компонента.

В любом случае, я бы посоветовал вам создать свой собственный хук useTimeout, чтобы вы могли DRY и упростить свой код, используя setTimeoutдекларативно, как Дэн Абрамов предлагает для setInterval в Делаем setInterval декларативным с помощью React Hooks, что очень похоже:

function useTimeout(callback, delay) {
  const timeoutRef = React.useRef();
  const callbackRef = React.useRef(callback);

  // Remember the latest callback:
  //
  // Without this, if you change the callback, when setTimeout kicks in, it
  // will still call your old callback.
  //
  // If you add `callback` to useEffect's deps, it will work fine but the
  // timeout will be reset.

  React.useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Set up the timeout:

  React.useEffect(() => {
    if (typeof delay === 'number') {
      timeoutRef.current = window.setTimeout(() => callbackRef.current(), delay);

      // Clear timeout if the components is unmounted or the delay changes:
      return () => window.clearTimeout(timeoutRef.current);
    }
  }, [delay]);

  // In case you want to manually clear the timeout from the consuming component...:
  return timeoutRef;
}

const App = () => {
  const [isLoading, setLoading] = React.useState(true);
  const [showLoader, setShowLoader] = React.useState(false);
  
  // Simulate loading some data:
  const fakeNetworkRequest = React.useCallback(() => {
    setLoading(true);
    setShowLoader(false);
    
    // 50% of the time it will display the loder, and 50% of the time it won't:
    window.setTimeout(() => setLoading(false), Math.random() * 4000);
  }, []);
  
  // Initial data load:
  React.useEffect(fakeNetworkRequest, []);
        
  // After 2 second, we want to show a loader:
  useTimeout(() => setShowLoader(true), isLoading ? 2000 : null);

  return (<React.Fragment>
    <button onClick = { fakeNetworkRequest } disabled = { isLoading }>
      { isLoading ? 'LOADING... ?' : 'LOAD MORE ?' }
    </button>
    
    { isLoading && showLoader ? <div className = "loader"><span className = "loaderIcon">?</span></div> : null }
    { isLoading ? null : <p>Loaded! ✨</p> }
  </React.Fragment>);
}

ReactDOM.render(<App />, document.querySelector('#app'));
body,
button {
  font-family: monospace;
}

body, p {
  margin: 0;
}

#app {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
}

button {
  margin: 32px 0;
  padding: 8px;
  border: 2px solid black;
  background: transparent;
  cursor: pointer;
  border-radius: 2px;
}

.loader {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 128px;
  background: white;
}

.loaderIcon {
  animation: spin linear infinite .25s;
}

@keyframes spin {
  from { transform:rotate(0deg) }
  to { transform:rotate(360deg) }
}
<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>

Помимо создания более простого и чистого кода, это позволяет вам автоматически очищать тайм-аут, передавая delay = null, а также возвращает идентификатор тайм-аута, если вы хотите отменить его вручную (это не рассматривается в сообщениях Дэна).

Если вы ищете аналогичный ответ для setInterval, а не для setTimeout, проверьте это: https://stackoverflow.com/a/59274004/3723993.

Вы также можете найти декларативную версию setTimeout и setInterval, useTimeout и useInterval, несколько дополнительных ловушек, написанных на TypeScript в https://www.npmjs.com/package/@swyg/corre.

@mystrdat Это ☝️ может ответить на ваш вопрос о том, как сбросить таймер при изменении некоторых свойств. В этом примере просто используйте эти реквизиты для передачи delay или null в useInterval. Если вы пройдете null, тайм-аут будет очищен для вас.

Danziger 10.12.2019 21:21

@loopmode То же самое. Это ☝️ может ответить на ваш вопрос об очистке таймера при изменении некоторых свойств.

Danziger 10.12.2019 21:23

Я написал хук реакции, чтобы больше никогда не приходилось иметь дело с тайм-аутами. работает так же, как React.useState ():

const [showLoading, setShowLoading] = useTimeoutState(false, {timeout: 5000})

// will set show loading after 5000ms
setShowLoading(true)
// overriding and timeouts after 1000ms
setShowLoading(true, { timeout: 1000})

Установка нескольких состояний обновит тайм-аут, и он истечет после того же миллисекунды, что и последний установленный setState.

Vanilla js (не тестировался, есть машинописная версия):

import React from "react"

// sets itself automatically to default state after timeout MS. good for setting timeouted states for risky requests etc.
export const useTimeoutState = (defaultState, opts) => {
  const [state, _setState] = React.useState(defaultState)
  const [currentTimeoutId, setCurrentTimeoutId] = React.useState()

  const setState = React.useCallback(
    (newState: React.SetStateAction, setStateOpts) => {
      clearTimeout(currentTimeoutId) // removes old timeouts
      newState !== state && _setState(newState)
      if (newState === defaultState) return // if already default state, no need to set timeout to set state to default
      const id = setTimeout(
        () => _setState(defaultState),
        setStateOpts?.timeout || opts?.timeout
      ) 
      setCurrentTimeoutId(id)
    },
    [currentTimeoutId, state, opts, defaultState]
  )
  return [state, setState]
}

Машинопись:

import React from "react"
interface IUseTimeoutStateOptions {
  timeout?: number
}
// sets itself automatically to default state after timeout MS. good for setting timeouted states for risky requests etc.
export const useTimeoutState = <T>(defaultState: T, opts?: IUseTimeoutStateOptions) => {
  const [state, _setState] = React.useState<T>(defaultState)
  const [currentTimeoutId, setCurrentTimeoutId] = React.useState<number | undefined>()
  // todo: change any to React.setStateAction with T
  const setState = React.useCallback(
    (newState: React.SetStateAction<any>, setStateOpts?: { timeout?: number }) => {
      clearTimeout(currentTimeoutId) // removes old timeouts
      newState !== state && _setState(newState)
      if (newState === defaultState) return // if already default state, no need to set timeout to set state to default
      const id = setTimeout(
        () => _setState(defaultState),
        setStateOpts?.timeout || opts?.timeout
      ) as number
      setCurrentTimeoutId(id)
    },
    [currentTimeoutId, state, opts, defaultState]
  )
  return [state, setState] as [
    T,
    (newState: React.SetStateAction<T>, setStateOpts?: { timeout?: number }) => void
  ]
}```
const[seconds, setSeconds] = useState(300);

function TimeOut() {
useEffect(() => {
    let interval = setInterval(() => {
        setSeconds(seconds => seconds -1);
    }, 1000);

    return() => clearInterval(interval);
}, [])

function reset() {
  setSeconds(300); 
} 

return (
    <div>
        Count Down: {seconds} left
        <button className = "button" onClick = {reset}>
           Reset
        </button>
    </div>
)
}

Обязательно импортируйте useState и useEffect. Также добавьте логику для остановки таймера на 0.

Вы думали об остановке интервала при достижении 0?

RegarBoy 27.11.2021 14:02

Если вы хотите создать такую ​​кнопку, как «start», то использование хука «useInterval» может не подойти, поскольку response не позволяет вызывать хуки, кроме как в верхней части компонента.

export default function Loading() {
  // if data fetching is slow, after 1 sec i will show some loading animation
  const [showLoading, setShowLoading] = useState(true)
  const interval = useRef();

  useEffect(() => {
      interval.current = () => setShowLoading(true);
  }, [showLoading]);

  // make a function like "Start"
  // const start = setInterval(interval.current(), 1000)

  setInterval(() => interval.current(), 1000);

  console.info('this message will render  every second')
  return 1
}

Если ваш таймаут находится в "конструкции if" попробуйте это:

useEffect(() => {
    let timeout;

    if (yourCondition) {
      timeout = setTimeout(() => {
        // your code
      }, 1000);
    } else {
      // your code
    }

    return () => {
      clearTimeout(timeout);
    };
  }, [yourDeps]);
export const useTimeout = () => {
    const timeout = useRef();
    useEffect(
        () => () => {
            if (timeout.current) {
                clearTimeout(timeout.current);
                timeout.current = null;
            }
        },
        [],
    );
    return timeout;
};

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

const timeout = useTimeout();
timeout.current = setTimeout(your conditions) 

Используйте setTimeout в ваших компонентах React, чтобы выполнить функцию или блок кода через определенный период времени. Давайте посмотрим, как использовать setTimeout в React. Также существует аналогичный метод под названием setInterval

useEffect(() => {
  const timer = setTimeout(() => {
    console.info('This will run after 1 second!')
  }, 1000);
  return () => clearTimeout(timer);
}, []);

В случае интервалов, чтобы избежать постоянного присоединения (монтирования) и отсоединения (размонтирования) метода setInterval к циклу событий с использованием перехватчика useEffect в примерах, приведенных другими, вы можете вместо этого использовать useReducer.

Представьте себе сценарий, в котором с учетом seconds и minutes вы должны отсчитывать время ... Ниже мы получили функцию reducer, которая выполняет обратный отсчет.

const reducer = (state, action) => {
  switch (action.type) {
    case "cycle":
      if (state.seconds > 0) {
        return { ...state, seconds: state.seconds - 1 };
      }
      if (state.minutes > 0) {
        return { ...state, minutes: state.minutes - 1, seconds: 60 };
      }
    case "newState":
      return action.payload;
    default:
      throw new Error();
  }
}

Теперь все, что нам нужно сделать, это отправить действие cycle в каждый интервал:

  const [time, dispatch] = useReducer(reducer, { minutes: 0, seconds: 0 });
  const { minutes, seconds } = time;

  const interval = useRef(null);
  
  //Notice the [] provided, we are setting the interval only once (during mount) here.
  useEffect(() => {
    interval.current = setInterval(() => {
      dispatch({ type: "cycle" });
    }, 1000);
    // Just in case, clear interval on component un-mount, to be safe.
    return () => clearInterval(interval.current);
  }, []);

  //Now as soon as the time in given two states is zero, remove the interval.
  useEffect(() => {
    if (!minutes && !seconds) {
      clearInterval(interval.current);
    }
  }, [minutes, seconds]);
  // We could have avoided the above state check too, providing the `clearInterval()`
  // inside our reducer function, but that would delay it until the next interval.

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