Код здесь: https://codesandbox.io/s/nw4jym4n0
export default ({ name }: Props) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearInterval(interval);
};
});
return <h1>{counter}</h1>;
};
Проблема заключается в повторном рендеринге каждого триггера setCounter, поэтому интервал сбрасывается и создается заново. Это может выглядеть нормально, поскольку состояние (счетчик) продолжает увеличиваться, однако оно может зависнуть при объединении с другими хуками.
Как правильно это сделать? В компоненте класса это просто с переменной экземпляра, содержащей интервал.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Вы можете указать пустой массив в качестве второго аргумента для useEffect, чтобы функция запускалась только один раз после первоначального рендеринга. Из-за того, как работают замыкания, это заставит переменную counter всегда ссылаться на начальное значение. Затем вы можете вместо этого использовать функциональную версию setCounter, чтобы всегда получать правильное значение.
Пример
const { useState, useEffect } = React;
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter => counter + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <h1>{counter}</h1>;
};
ReactDOM.render(
<App />,
document.getElementById('root')
);<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 = "root"></div>Более универсальным подходом было бы создание новой пользовательской ловушки, которая хранит функцию в ref и создает новый интервал, только если delay должен измениться, как это делает Дэн Абрамов в своем замечательном сообщении в блоге "Делаем setInterval декларативным с помощью React Hooks".
Пример
const { useState, useEffect, useRef } = React;
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
let id = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [counter, setCounter] = useState(0);
useInterval(() => {
setCounter(counter + 1);
}, 1000);
return <h1>{counter}</h1>;
};
ReactDOM.render(
<App />,
document.getElementById('root')
);<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 = "root"></div>Как уже показывает другой ответ от @YangshunTay, можно сделать так, чтобы обратный вызов useEffect запускался только один раз и работал аналогично componentDidMount. В этом случае необходимо использовать функцию обновления состояния из-за ограничений, накладываемых областями функций, иначе обновленный counter не будет доступен внутри обратного вызова setInterval.
Альтернативный вариант - запускать обратный вызов useEffect при каждом обновлении счетчика. В этом случае setInterval следует заменить на setTimeout, а обновления должны быть ограничены обновлениями counter:
export default ({ name }: Props) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [counter]);
return <h1>{counter}</h1>;
};
Это не идеально, так как setTimeout будет запускаться при каждом рендеринге, и в этом нет необходимости.
Я не вижу в этом ничего плохого. Проблемы с производительностью отсутствуют. Рекурсивный setTimeout - это обычная замена setInterval в JS, когда это оправдано. Функция обновления состояния отлично выглядит в setInterval в этом конкретном случае, то есть для простого приращения. Это было бы совсем иначе, если бы было задействовано несколько государств.
Правильный способ сделать это - запустить эффект только один раз. Поскольку вам нужно запустить эффект только один раз, потому что во время монтирования вы можете передать пустой массив в качестве второго аргумента для достижения.
Однако вам нужно будет изменить setCounter, чтобы использовать предыдущее значение counter. Причина в том, что обратный вызов, переданный в закрытие setInterval, обращается только к переменной counter в первом рендеринге, у него нет доступа к новому значению counter в последующем рендеринге, потому что useEffect() не вызывается во второй раз; counter всегда имеет значение 0 в обратном вызове setInterval.
Как и в случае с setState, с которым вы знакомы, хуки состояния имеют две формы: одну, в которой он принимает обновленное состояние, и форму обратного вызова, в которую передается текущее состояние. Вы должны использовать вторую форму и считывать последнее значение состояния в setState. обратный вызов, чтобы убедиться, что у вас есть последнее значение состояния, прежде чем увеличивать его.
function Counter() {
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setCounter(prevCount => prevCount + 1); // <-- Change this line!
}, 1000);
return () => {
clearInterval(timer);
};
}, []); // Pass in empty array to run effect only once!
return (
<div>Count: {counter}</div>
);
}
ReactDOM.render(<Counter />, 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>
Вам действительно нужно было сохранить его здесь с помощью useRef? Не могли бы вы просто вызвать обратный вызов, не ссылаясь на него?