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

Я пробую новые React Hooks и у меня есть компонент 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>

Ответ 1

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

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

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

Бонус: альтернативные подходы

Дан Абрамов подробно описывает тему использования setInterval с хуками в своем блоге и предлагает альтернативные способы решения этой проблемы. Очень рекомендую прочитать это!

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>

Ответ 2

Функция 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 являются приемлемыми.

Ответ 3

Альтернативным решением будет использование 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>

Ответ 4

Скажите 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>

Ответ 5

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

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

Например:

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

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

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);

Ответ 6

Если кому-то нужно управлять очередью

Допустим, для показа уведомлений с интервалом в 3 секунды (первым пришел, первым вышел) с возможностью отправлять новые сообщения в любое время.

Пример Codesandbox.

import React, {useState, useRef, useEffect} from "react";
import ReactDOM from "react-dom";

import "./styles.css";

let x = 1 // for testing
const fadeTime = 3000 // 3 sec 

function App() {
  // our messages array in what we can push at any time
  const [queue, setQueue] = useState([]) 

  // our shiftTimer that will change every 3 sec if array have items
  const [shiftTimer, setShiftTimer] = useState(Date.now())

  // reference to timer
  const shiftTimerRef = useRef(null)

  // here we start timer if it was mot started yet
  useEffect(() => {
    if (shiftTimerRef.current === null && queue.length != 0) {
      startTimer()
    }
  }, [queue])

  // here we will shift first message out of array (as it was already seen)
  useEffect(() => {
    shiftTimerRef.current = null
    popupShift()
  }, [shiftTimer])

  function startTimer() {
    shiftTimerRef.current = setTimeout(() => {
      setShiftTimer(Date.now)
    }, fadeTime )
  }

  function startTimer() {
    shiftTimerRef.current = setTimeout(() => setShiftTimer(Date.now), fadeTime )
  }

  function popupPush(newPopup) {
    let newQueue = JSON.parse(JSON.stringify(queue))
    newQueue.push(newPopup)
    setQueue(newQueue)
  }

  function popupShift() {
    let newQueue = JSON.parse(JSON.stringify(queue))
    newQueue.shift()
    setQueue(newQueue)
  }

  return (
    <div>
      <button onClick={() => popupPush({ message: x++ })}>Push new message</button>
      <div>{JSON.stringify(queue)}</div>
    </div>
  )
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);