Стан не оновлюється при використанні гачка стану React у setInterval


110

Я випробовую новий 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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

Відповіді:


159

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

timeзавжди має значення 0 у setIntervalзворотному виклику.

Як і 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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>


3
Це дуже корисна інформація. Дякую Яншону!
Tholle

8
Ласкаво просимо, я витратив час, дивлячись на код, лише для того, щоб дізнатися, що припустився дуже елементарної (але, мабуть, неочевидної?) Помилки, і хотів поділитися цим із спільнотою
Yangshun Tay

3
ТАК у найкращому випадку 👍
Патрік Хунд

42
Ха-ха, я прийшов сюди, щоб підключити мій пост, і це вже у відповіді!
Ден Абрамов

2
Чудовий допис у блозі. Відкриття очей.
Бенджамінадк,

19

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підходи є прийнятними.


13

Як зазначали інші, проблема полягає в тому, що він 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хук, щоб ви могли СУХИТИ та спрощувати свій код, використовуючи setInterval декларативно , як це пропонує Ден Абрамов тут, під час створення setInterval Declarative with 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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/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 .


5

Альтернативним рішенням було б використання 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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>


Чому useEffectтут викликається кілька разів для оновлення часу, тоді як масив залежностей порожній, а це означає, що useEffectслід викликати лише перший раз, коли компонент / програма відображається?
BlackMath

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

3

Зробіть, як показано нижче, це чудово працює.

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

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

//call increment function
increment(count);

У count => count + 1мене спрацював зворотний дзвінок, дякую!
Nickofthyme

1

Ці рішення для мене не працюють, тому що мені потрібно отримати змінну та зробити деякі речі, а не просто оновити її.

Я отримую обхідний шлях, щоб отримати оновлене значення гачка з обіцянкою

Наприклад:

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

За допомогою цього я можу отримати значення всередині функції setInterval, як це

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);

0

Скажіть, що 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/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>


2
Проблема цього полягає в тому, що таймер буде очищений і скинутий після кожної countзміни.
Сумаїл

І тому так setTimeout()віддають перевагу, як зазначив Естус
Хаїм Фрідман
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.