Когда в обработчике кликов используются два установщика useState, работает только один.

Я сделал небольшой компонент секундомера, используя хуки в response. Это минимальный код, демонстрирующий проблему.

Посмотрите на функцию с именем resetTicks. У него есть два установщика setTicks и setTicking, и работает только setTicking, т.е. часы останавливаются, и что интересно, если я снова нажимаю кнопку, он сбрасывает часы. Я безуспешно пытался переупорядочить вызовы обоих сеттеров.

const StopWatch = () => {
  const [ticks,setTicks] = useState(0);
  const [ticking,setTicking] = useState(false);
  useEffect(() => {
    setTimeout(() => {
      if (ticking) setTicks(ticks + 1);
    },10);
  },[ticks,ticking]);

  const toggleTicking = e => {
    setTicking(!ticking);
  }

  const resetTicks = e => {
    // these two setters are causing the issue
    // only the setTicking is actually showing effect. I have tried switching 
    // their order but nothing works.
    setTicking(false); 
    setTicks(0);
  }

  const min = Math.floor(ticks / 6000);
  const sec = Math.floor((ticks - (min * 6000)) / 100);
  const centis = ticks % 100;
  return (
    <WatchWrapper>
      <WatchDisplay>
        <span>{min < 10 ? '0': ''}{min}</span>
        <span>:</span>
        <span>{sec < 10 ? '0': ''}{sec}</span>
        <span>:</span>
        <span>{centis < 10 ? '0' : ''}{centis}</span>
        </WatchDisplay>
        <WatchControls>
          <WatchBtn onClick={toggleTicking}>
            {ticking ? 'stop' : 'play_arrow'} 
          </WatchBtn>
          <WatchBtn onClick={resetTicks}>refresh</WatchBtn>
        </WatchControls>
      </WatchWrapper>
    )
}

person Sayam Qazi    schedule 03.07.2020    source источник
comment
Первоначально вы определили значение тиков как 0, а после щелчка также определите его setTicks(0). Следовательно, компонент не перерисовывается. Попробуйте установить для него другое значение, кроме 0;   -  person Durgesh Pal    schedule 03.07.2020
comment
@DurgeshPal Нет. Я нажимаю кнопку сброса, когда часы идут, поэтому тики уже изменены из-за множества вызовов setTicks с разными значениями.   -  person Sayam Qazi    schedule 03.07.2020
comment
да. Но я думаю, что каким-то образом он устанавливает значение 0 между ними.   -  person Durgesh Pal    schedule 03.07.2020


Ответы (3)


Это непростой вопрос, вы должны понять, что происходит, из console.log:

true
56
true
57
true
58
true
59
false
0
false
60

Он действительно получает значение 0, но, очевидно, в какой-то момент старый setTimeout, который был запланирован срабатыванием, имел закрытие на старом значении тика, когда оно было 60, поэтому он сбрасывает его обратно.

Увеличьте тайм-аут, чтобы сказать, что 3 секунды делают console.log(ticking, ticks) в рендере, и вам должно быть более очевидно, в чем проблема.

person Giorgi Moniava    schedule 03.07.2020
comment
О боже. У тебя такой зоркий глаз. Почему я даже не подумал об этом. Забавно то, что у меня было это в голове, когда я писал эффект, что, чувак, будет сценарий, когда этот тайм-аут срабатывает заранее, и я столкнусь с проблемой, но это не пришло мне в голову, когда возникла настоящая проблема. - person Sayam Qazi; 03.07.2020

Это из-за состояния гонки между setTicks установщиком и асинхронным вызовом обратного вызова внутри setTimeout. Установщик setTicks обновляет счетчик тиков, но старый счетчик тиков был сохранен в области setTimeout. Таким образом, setTimeout вызывает обратный вызов, передавая ему старое значение ticks в качестве параметра. Вам необходимо очистить setTimeout при отключении компонента, чтобы предотвратить это:

  useEffect(() => {
    const timeout = setTimeout(() => {
      if (ticking) setTicks(ticks + 1);
    }, 10);
    return () => clearTimeout(timeout);
  }, [ticking, ticks]);
person B_Joker    schedule 03.07.2020

Чтобы убедиться, что нет состояния гонки, вы можете попытаться сбросить setTimeout перед resetTicks, создав React Reference для таймера.

const [ticks,setTicks] = React.useState(0);
const [ticking,setTicking] = React.useState(false);
const timer = React.createRef();
React.useEffect(() => {
    timer.current = setTimeout(() => {
        if (ticking) setTicks(ticks + 1);
    },10);
},[ticks,ticking, timer]);

const toggleTicking = e => {
    setTicking(!ticking);
}

const resetTicks = e => {
    clearTimeout(timer.current);
    setTicking(false); 
    setTicks(0);
}

Протестируйте с помощью Codesandbox здесь:

https://codesandbox.io/embed/test-reset-race-condition-g53l5?fontsize=14&hidenavigation=1&theme=dark&view=editor

person Jannis Schönleber    schedule 03.07.2020