Состояние не обновляется при использовании обработчика состояния React в setInterval

Я пробую новый Реагировать на хуки и имею компонент Clock со счетчиком, который должен увеличиваться каждую секунду. Однако значение не превышает единицы.

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, 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>

Есть прекрасные объяснения, почему это происходит. В случае, если кто-то хочет также получить значение stackoverflow.com/a/57679222/4427870, это сильно недооцененный прием.

Rishav 28.03.2021 21:30
Поведение ключевого слова "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) для оценки ваших знаний,...
173
1
84 526
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

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

Причина в том, что обратный вызов, переданный в закрытие setInterval, обращается только к переменной time в первом рендере, у него нет доступа к новому значению time в последующем рендеринге, потому что useEffect() не вызывается во второй раз.

time всегда имеет значение 0 в обратном вызове setInterval.

Как и в случае с setState, с которым вы знакомы, перехватчики состояния имеют две формы: одну, в которой он принимает обновленное состояние, и форму обратного вызова, в которую передается текущее состояние. Вы должны использовать вторую форму и считывать последнее значение состояния в setState. обратный вызов, чтобы убедиться, что у вас есть последнее значение состояния, прежде чем увеличивать его.

Bonus: Alternative Approaches

Dan Abramov, goes in-depth into the topic about using setInterval with hooks in his blog post and provides alternative ways around this issue. Highly recommend reading it!

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(prevTime => prevTime + 1); // <-- Change this line!
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, 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>

@YangshunTay Если я просто хочу прочитать значение состояния в setInterval, как мне это сделать?

neosarchizo 17.02.2020 02:36

@neosarchizo Вы читали пост Дэна? overrected.io/making-setinterval-declarative-with-react-hoo‌ ks. Если вы просто хотите прочитать его, вы можете прочитать обновленное значение как часть рендеринга внизу. Если вы хотите вызвать побочные эффекты, вы можете добавить перехватчик useEffect() и добавить это состояние в массив зависимостей.

Yangshun Tay 17.02.2020 04:34

Как бы это выглядело, если бы вы хотели периодически выводить текущее состояние с помощью console.info в функции setInterval?

user3579222 22.03.2020 11:28

Я хочу прочитать время (в setInterval) и обновить, если оно больше, чем какое-то время. Как этого добиться?

artsnr 22.01.2021 18:47

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

artsnr 22.01.2021 19:03

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

Daniel 03.04.2021 04:47

Потратил примерно 2 часа, чтобы найти это решение и понимание ....

Berke 27.08.2021 16:44

Функция useEffect оценивается только один раз при монтировании компонента, если предоставляется пустой список ввода.

Альтернативой setInterval является установка нового интервала с setTimeout каждый раз при обновлении состояния:

  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = setTimeout(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      clearTimeout(timer);
    };
  }, [time]);

Влияние setTimeout на производительность незначительно, и его можно игнорировать. Если компонент не чувствителен ко времени до точки, где вновь установленные тайм-ауты вызывают нежелательные эффекты, приемлемы подходы как setInterval, так и setTimeout.

Альтернативным решением было бы использовать useReducer, так как ему всегда будет передаваться текущее состояние.

function Clock() {
  const [time, dispatch] = React.useReducer((state = 0, action) => {
    if (action.type === 'add') return state + 1
    return state
  });
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      dispatch({ type: 'add' });
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, 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>

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

BlackMath 26.09.2020 01:20

@BlackMath Функция внутри useEffect вызывается только один раз, когда компонент действительно сначала отрисовывается. Но внутри него находится setInterval, который регулярно меняет время. Предлагаю вам немного почитать о setInterval, после этого все станет яснее! developer.mozilla.org/en-US/docs/Web/API/…

Bear-Foot 27.09.2020 14:48

Сообщите React о повторном рендеринге при изменении времени.

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, [time]);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, 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.

sumail 21.03.2019 07:51

И поскольку setTimeout() предпочтительнее, как указывает Эстус

Chayim Friedman 16.07.2020 11:33

Эти решения не работают для меня, потому что мне нужно получить переменную и сделать что-то, а не просто обновить ее.

У меня есть обходной путь, чтобы получить обновленное значение хука с обещанием

Например:

async function getCurrentHookValue(setHookFunction) {
  return new Promise((resolve) => {
    setHookFunction(prev => {
      resolve(prev)
      return prev;
    })
  })
}

С этим я могу получить значение внутри функции setInterval следующим образом

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);

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

Emile Bergeron 20.09.2021 20:19

Как указывали другие, проблема в том, что useState вызывается только один раз (как deps = []) для установки интервала:

React.useEffect(() => {
    const timer = window.setInterval(() => {
        setTime(time + 1);
    }, 1000);

    return () => window.clearInterval(timer);
}, []);

Затем каждый раз, когда тикает setInterval, он фактически вызывает setTime(time + 1), но time всегда будет хранить значение, которое оно имело изначально, когда был определен обратный вызов (закрытие) setInterval.

Вы можете использовать альтернативную форму установщика useState и предоставить обратный вызов, а не фактическое значение, которое вы хотите установить (как и в случае с setState):

setTime(prevTime => prevTime + 1);

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

function useInterval(callback, delay) {
  const intervalRef = React.useRef();
  const callbackRef = React.useRef(callback);

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

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

  // Set up the interval:

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

      // Clear interval if the components is unmounted or the delay changes:
      return () => window.clearInterval(intervalRef.current);
    }
  }, [delay]);
  
  // Returns a ref to the interval ID in case you want to clear it manually:
  return intervalRef;
}


const Clock = () => {
  const [time, setTime] = React.useState(0);
  const [isPaused, setPaused] = React.useState(false);
        
  const intervalRef = useInterval(() => {
    if (time < 10) {
      setTime(time + 1);
    } else {
      window.clearInterval(intervalRef.current);
    }
  }, isPaused ? null : 1000);

  return (<React.Fragment>
    <button onClick = { () => setPaused(prevIsPaused => !prevIsPaused) } disabled = { time === 10 }>
        { isPaused ? 'RESUME ⏳' : 'PAUSE ?' }
    </button>

    <p>{ time.toString().padStart(2, '0') }/10 sec.</p>
    <p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
  </React.Fragment>);
}

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

body, p {
  margin: 0;
}

p + p {
  margin-top: 8px;
}

#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;
}
<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, а также возвращает идентификатор интервала, если вы хотите отменить его вручную (это не рассматривается в сообщениях Дэна).

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

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

Вы также можете найти декларативную версию setTimeout и setInterval, useTimeout и useInterval, а также специальный перехватчик useThrottledCallback, написанный на TypeScript в https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a.

Сделайте, как показано ниже, он отлично работает.

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

async function increment(count,value) {
    await setCount(count => count + 1);
  }

//call increment function
increment(count);

Где в вашем ответе используется setInterval?

Vasanth 10.05.2021 19:49

Параметры increment здесь тоже бесполезны.

Emile Bergeron 20.09.2021 20:20

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

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

export default function App() {
  const initalState = 0;
  const [count, setCount] = useState(initalState);
  const counterRef = useRef(initalState);

  useEffect(() => {
    counterRef.current = count;
  })

  useEffect(() => {
    setInterval(() => {
      setCount(counterRef.current + 1);
    }, 1000);
  }, []);

  return (
    <div className = "App">
      <h1>The current count is:</h1>
      <h2>{count}</h2>
    </div>
  );
}

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

Я скопировал код из этого блога. Все кредиты владельцу. https://overrected.io/making-setinterval-declarative-with-react-hooks/

Единственное, что я адаптировал этот код React к коду React Native, поэтому, если вы являетесь нативным кодировщиком, просто скопируйте его и адаптируйте к тому, что вы хотите. Адаптировать очень легко!

import React, {useState, useEffect, useRef} from "react";
import {Text} from 'react-native';

function Counter() {

    function useInterval(callback, delay) {
        const savedCallback = useRef();
      
        // Remember the latest function.
        useEffect(() => {
          savedCallback.current = callback;
        }, [callback]);
      
        // Set up the interval.
        useEffect(() => {
          function tick() {
            savedCallback.current();
          }
          if (delay !== null) {
            let id = setInterval(tick, delay);
            return () => clearInterval(id);
          }
        }, [delay]);
      }

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

  useInterval(() => {
    // Your custom logic here
    setCount(count + 1);
  }, 1000);
  return <Text>{count}</Text>;
}

export default Counter;

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