SetTimeout для this.state против useState

Когда я использую компонент класса, у меня есть код:

setTimeout(() => console.info(this.state.count), 5000);

Когда я использую хук:

const [count, setCount] = useState(0);
setTimeout(() => console.info(count), 5000);

Если я активирую setTimeout, то изменю count на 1 до истечения времени ожидания (5000ms), компонент класса будет console.info(1) (самое новое значение), а для useState это console.info(0) (значение при тайм-ауте регистрации).
Почему это происходит?

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
9
0
6 852
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Для useState он создает тайм-аут, используя count в первый раз. Он получает доступ к значению count через closure. Когда мы устанавливаем новое значение с помощью setCount, компонент перерисовывается, но не изменяет значение, переданное таймауту.
Мы можем использовать const count = useRef(0) и перейти к таймауту count.current. При этом всегда будет использоваться новейшее значение count.
Перейдите по этой ссылке для получения дополнительной информации.

Тайм-ауты не очень хорошо сочетаются с реакцией модели декларативного программирования. В функциональных компонентах каждый рендер представляет собой один кадр во времени. Они никогда не меняются. При обновлении состояния все переменные состояния создаются локально заново и не перезаписывают старые закрытые переменные.

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

Единственный способ вырваться из этой модели — реф. Или компоненты класса, где состояние эффективно похоже на ссылки, где экземпляр (this) является контейнером ссылки. Рефы позволяют осуществлять кросс-рендеринг и блокировку замыканий. Используйте экономно и с осторожностью.

У Дэна Абрамова есть фантастическая статья, объясняющая все это и хук, который решает эту проблему. Как вы правильно ответили, проблема вызвана устаревшим закрытием. Решение действительно включает использование refs.

Ответ принят как подходящий

Обновленная версия:

Вопрос: Разница в поведении переменной React State внутри setTimeout/setInterval для функций и компонентов класса?

Случай 1: переменная состояния в функциональном компоненте (устаревшее закрытие):

const [value, setValue] = useState(0)

useEffect(() => {
  const id = setInterval(() => {
    // It will always print 0 even after we have changed the state (value)
    // Reason: setInterval will create a closure with initial value i.e. 0
    console.info(value)
  }, 1000)
  return () => {
    clearInterval(id)
  }
}, [])

Случай 2: переменная состояния в компоненте класса (без устаревшего закрытия):

constructor(props) {
  super(props)
  this.state = {
    value: 0,
  }
}

componentDidMount() {
  this.id = setInterval(() => {
    // It will always print current value from state
    // Reason: setInterval will not create closure around "this"
    // as "this" is a special object (refernce to instance)
    console.info(this.state.value)
  }, 1000)
}

Случай 3: Давайте попробуем создать устаревшее замыкание вокруг this

// Attempt 1

componentDidMount() {
  const that = this // create a local variable so that setInterval can create closure
  this.id = setInterval(() => {
    console.info(that.state.value)
    // This, too, always print current value from state
    // Reason: setInterval could not create closure around "that"
    // Conclusion: Oh! that is just a reference to this (attempt failed)
  }, 1000)
}

Случай 4: Давайте снова попробуем создать устаревшее замыкание в компоненте класса

// Attempt 2

componentDidMount() {
  const that = { ...this } // create a local variable so that setInterval can create closure
  this.id = setInterval(() => {
    console.info(that.state.value)
    // Great! This always prints 0 i.e. the initial value from state
    // Reason: setInterval could create closure around "that"
    // Conclusion: It did it because that no longer is a reference to this,
    // it is just a new local variable which setInterval can close around
    // (attempt successful)
  }, 1000)
}

Случай 5: Давайте снова попробуем создать устаревшее замыкание в компоненте класса

// Attempt 3

componentDidMount() {
  const { value } = this.state // create a local variable so that setInterval can create closure
  this.id = setInterval(() => {
    console.info(value)
    // Great! This always prints 0 i.e. the initial value from state
    // Reason: setInterval created closure around value
    // Conclusion: It is easy! value is just a local variable so it will be closed
    // (attempt successful)
  }, 1000)
}

Случай 6: класс выиграл (без дополнительных усилий, чтобы избежать устаревшего закрытия). Но как избежать этого в функциональном компоненте?

// Let's find solution

const value = useRef(0)

useEffect(() => {
  const id = setInterval(() => {
    // It will always print the latest ref value
    // Reason: We used ref which gives us something like an instance field.
    // Conclusion: So, using ref is a solution
    console.info(value.current)
  }, 1000)
  return () => {
    clearInterval(id)
  }
}, [])

источник-1 , источник-2

Случай 6: Давайте найдем другое решение для функциональных компонентов

useEffect(() => {
  const id = setInterval(() => {
    // It will always print the latest state value
    // Reason: We used updater form of setState (which provides us latest state value)
    // Conclusion: So, using updater form of setState is a solution
    setValue((prevValue) => {
      console.info(prevValue)
      return prevValue
    })
  }, 1000)
  return () => {
    clearInterval(id)
  }
}, [])

Оригинальная версия:

Проблема вызвана замыканиями и может быть устранена с помощью ref. Но вот обходной путь, чтобы исправить это, то есть получить доступ к последнему значению state, используя форму «обновления» setState:

function App() {

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

  React.useEffect(() => {
    setTimeout(() => console.info('count after 5 secs: ', count, 'Wrong'), 5000)
  }, [])

  React.useEffect(() => {
    setTimeout(() => {
      let count
      setCount(p => { 
        console.info('p: ', p)
        count = p
        return p
       })
      console.info('count after 5 secs: ', count, 'Correct')
    }, 5000);
  }, [])

  return (<div>
    <button onClick = {() => setCount(p => p+1)}>Click me before 5 secs</button>
    <div>Latest count: {count}</div>
  </div>)
}

ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src = "https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src = "https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<body>
<div id = "mydiv"></div>
</body>

Это интересное решение. Недостатком является то, что использование сеттера для получения последнего значения также вызывает повторную визуализацию, поэтому весь побочный эффект setTimeout вызывает ненужную визуализацию. Это может привести к бесконечному циклу в некоторых обстоятельствах, например. если useEffect зависит от измененного счетчика.

deckele 05.03.2021 21:58

@deckele setState(prevValue => prevValue) не вызовет повторного рендеринга, потому что возвращается то же значение. React сделает: должен отобразить: Object.is(oldValue, newValue) => false. Так что ререндера не будет, если мы это сделаем setCount(p => { // do_something; return p})

Ajeet Shah 05.03.2021 22:09

В документах говорится, что дополнительных рендеров не будет, если результат setState такой же, как и предыдущее значение. Однако это не совсем правильно, вся функция перерисовывается во второй раз, и только потом выручает от отрисовки своих дочерних элементов: github.com/facebook/react/issues/14994 Тем не менее, вы правы в своем примере что использование setState безопасно внутри useEffect. Но использование этого метода снаружи в рендере вызовет бесконечный цикл. Даже несмотря на то, что текущее значение идентично предыдущему.

deckele 05.03.2021 22:58

@deckele хорошо. Может быть. Я не читал документы, когда сказал это - there will no re-render if we do setCount(p => { // do_something; return p}). Я протестировал его локально, а затем подумал о его написании. Я не знаю, что такое правда. В любом случае, примеры, предоставленные мной, предназначены для понимания цели (редко они будут реальным случаем). Вдобавок к этому, вероятно, нам меньше нужно беспокоиться о бесконечном повторном рендеринге с использованием setTimeout.

Ajeet Shah 05.03.2021 23:05

@deckele Я только что увидел ваше редактирование: But using this method outside in render will cause an infinite loop. Even though current value is identical to the previous one: И я проверил это, да, вы правы. Я никогда не знал этого, потому что я никогда не писал такой код, но вы правы :) Спасибо!

Ajeet Shah 05.03.2021 23:22

Объяснение

С функциональными компонентами каждый рендеринг является вызовом функции, создавая новое закрытие функции для этого конкретного вызова. Функциональный компонент закрывается функцией обратного вызова setTimeout, так что все в обратном вызове setTimeout обращается только к конкретному рендерингу, где он был вызван.

Многоразовое решение:

Использование Ref и доступ к нему только в рамках обратного вызова setTimeout даст вам значение, которое сохраняется между рендерингами.

Однако не так удобно использовать React Ref со значением, которое всегда обновляется, например счетчик. Вы отвечаете как за обновление значения, так и за повторный рендеринг. Обновление Ref не влечет за собой визуализацию компонента.

Мое решение для простоты использования состоит в том, чтобы объединить хуки useState и useRef в один хук «useStateAndRef». Таким образом, вы получаете сеттер, который получает как значение, так и ссылку для использования в асинхронных ситуациях, таких как setTimeout и setInterval:

import { useState, useRef } from "react";

function useStateAndRef(initial) {
  const [value, setValue] = useState(initial);
  const valueRef = useRef(value);
  valueRef.current = value;
  return [value, setValue, valueRef];
}

export default function App() {
  const [count, setCount, countRef] = useStateAndRef(0);
  function logCountAsync() {
    setTimeout(() => {
      const currentCount = countRef.current;
      console.info(`count: ${count}, currentCount: ${currentCount}`);
    }, 2000);
  }
  return (
    <div className = "App">
      <h1>useState with updated value</h1>
      <h2>count: {count}</h2>
      <button onClick = {() => setCount(prev => prev + 1)}>+</button>
      <button onClick = {logCountAsync}>log count async</button>
    </div>
  );
}

Ссылка на рабочую среду CodeSandbox: https://codesandbox.io/s/set-timeout-with-hooks-fdngm?file=/src/App.tsx

Другие вопросы по теме