Состояние не обновляется при использовании обработчика состояния 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);