react Компонент с таймером не обновляется таймер
Делаю компонент с таймером, но он добавляет к таймеру всего +1 и больше не работает, из за чего это может быть и как можно исправить?
import React, {useEffect, useState} from 'react';
const TimerPages = ( { updateTime, stopTimer, resetTimers}) => {
const [timer, setTimer] = useState(0);
const [resetTimer, setResetTimer] = useState(false);
const addTimer = () => {
setTimer(timer + 1);
}
useEffect(() => {
setInterval(() => {
addTimer()
}
, 1000)}, [])
useEffect(() => {
updateTime(timer);
}, [timer]})
return (
<h5 className='card-title'>Время игры: {timer}</h5>
);
};
export default TimerPages;
Ответы (1 шт):
И так самое важное тут - это понять, что компонента перерисовывается только при обновлении стейта (Как оказалось это не совсем правда). В этом случае (упрощённом естественно) всего один стейт - это timer
В первый раз компонента рендерится со стейтом 0 и мы видим в консоли
RenderПотом запускается
useEffect, в консоли видимUse effectи устанавливает бесконечный таймер. Тут очень важно понять, что функция вsetIntervalВСЕГДА будет видеть только те значения стейта, при которых он был вызван. В конкретном случае это значит, что при инициализацииtimer === 0, потому после ререндеринга, когда вtimerбудет новое значение, наша функцияaddTimerне будет об этом знатьЧерез время запускается
addTimer, мы в консоли видимtimer 0и устанавливаем новое значение равноеtimer + 1. И так у нас до обновленияtimer === 0, потому когда мы вставим новое значение т.е.0 + 1 = 1, то стейт обновится, произойдёт перерисовка и мы увидим в консолиRenderОпять через время запускается
addTimer, мы в консоли видимtimer 0и устанавливаем значение равноеtimer + 1, НО помним что в нашей функцииtimerвсегда равен 0, потому мы опять будем пытаться вставить значение 1. Предыдущее значение было 1 и мы опять пытаемся вставить 1, потому обновлении стейта не произойдёт. Тут нужна оговорка хоть мы и увидим в третий разRender- это не потому что обновилось значение, а просто React не гаранитрует, что не перерисует при одном и том же значении стейтаДальше мы бесконечно будем видеть в консоли
timer 0, потому что мы постоянно будем пытаться обновить значение с 1 на 1, React увидит, что новое значение равно предыдущему и не будет перерисовывать, потомуRenderв консоли мы больше не увидим
const {useEffect, useState} = React;
const App = () => {
const [timer, setTimer] = useState(0);
const addTimer = () => {
console.log('timer', timer);
setTimer(timer + 1);
}
useEffect(() => {
console.log('Use effect');
setInterval(addTimer, 4000);
}, [])
return <h5 className='card-title'> Время игры: {console.log('Render') || timer}</h5>;
};
ReactDOM.render(<App />, document.getElementById('root'));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>
Если не хотите ждать, то вот какой вывод консоли будет:
Render
Use effect
timer 0
Render
timer 0
Render - этот рендер не связан с тем что состояние обновилось
timer 0 (бесконечно раз)
Что очень важно при работе с таймерами - это всегда них очищать, как только они больше не нужны. Есть несколько решений этой проблемы:
Как по мне самое лучшее - это заменить
setIntervalнаsetTimeoutи в массив зависимостей вставитьtimer. Так мы всегда будем получать новое значение стейта и будем устанавливать таймер только после того как обновится компонента в отлии отsetInterval, который насильно будет вызывать функциюconst {useEffect, useState} = React; const App = () => { const [timer, setTimer] = useState(0); const addTimer = () => { setTimer(timer + 1); } useEffect(() => { const timeout = setTimeout(addTimer, 1000); return () => clearTimeout(timeout); }, [timer]) return <h5 className='card-title'> Время игры: {timer}</h5>; }; ReactDOM.render(<App />, document.getElementById('root'));<div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>Можно вызывать не просто
addTimer(timer + 1), а вызыватьaddTimer(t => t + 1). В этом случае нам не важно что в переменнойtimer, мы смотрим на текущее состояние стейта, которое приходит на вход вt, потому всё будет работать корректноconst {useEffect, useState} = React; const App = () => { const [timer, setTimer] = useState(0); const addTimer = () => { setTimer(t => t + 1); } useEffect(() => { const interval = setInterval(addTimer, 1000); return () => clearInterval(interval); }, []) return <h5 className='card-title'> Время игры: {timer}</h5>; }; ReactDOM.render(<App />, document.getElementById('root'));<div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>