Реакция ссылки на состояние в onClick неверна - возможна проблема закрытия? - PullRequest
1 голос
/ 06 января 2020

Вот урезанная версия компонента React, который у меня есть. Это домашний экран с вводной анимацией, который автоматически завершается и исчезает по истечении времени ожидания, но также может быть пропущен пользователем.

import React, { useEffect, useState } from "react";

export function Home() {
  const [introEnded, setIntroEnded] = useState(false);

  // function to end intro animation
  function endIntro() {
    console.log(introEnded); // THIS IS WRONG THE SECOND TIME FUNCTION IS CALLED
    if (!introEnded) {
      setIntroEnded(true);
      // do various other stuff
    }
    // do various other stuff
  }

  // if user does not interact, intro ends automatically
  useEffect(() => {
    setTimeout(function() {
      endIntro();
    }, 4000);
  }, []);

  // user can skip intro on click
  return (
    <div>
      <div onClick={endIntro}>Skip Intro</div>
    </div>
  );
}

Проблема заключается в том, что после того, как пользователь пропускает, когда тайм-аут срабатывает и вызывает endIntro() во второй раз, состояние introEnded неверно. Я ясно установил его в true при первом вызове endIntro(), но второй вызов endIntro(), похоже, ссылается на более старое состояние.

Исследования показывают, что это какая-то проблема закрытия, но я могу не нашли решение.

Что происходит, и как мне это исправить? Спасибо!

Ответы [ 3 ]

1 голос
/ 06 января 2020

Да, это проблема закрытия Javascript. Значение переменной introEnded является ложным, когда вызывается useEffect, и этого нельзя избежать. Чтобы достичь этой цели, нам нужно написать некоторый ручной код, как показано ниже.

export function Home() {
  const [introEnded, setIntroEnded] = useState(false);
  const [elapsed, setElapsed] = useState(0);
  const start = React.useMemo(() => {
    return new Date().getTime();
  }, []);

  // function to end intro animation
  const endIntro = React.useCallback(() => {
    console.log(introEnded); // THIS IS WRONG THE SECOND TIME FUNCTION IS CALLED
    if (!introEnded) {
      setIntroEnded(true);
      // do various other stuff
    }
    // do various other stuff
  }, [introEnded]);

  // if user does not interact, intro ends automatically
  useEffect(() => {
    // console.log(elapsed);
    let timerId = -1
    if (4000 > elapsed) {
      timerId = setTimeout(function() {
        endIntro();
      }, 4000 - elapsed);
    }

    return () => {
      setElapsed(new Date().getTime() - start);
      if (timerId > 0) clearTimeout(timerId);
    };
  }, [elapsed, endIntro, start]);

  // user can skip intro on click
  return (
    <div>
      <div onClick={endIntro}>Skip Intro</div>
    </div>
  );
}

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

// custom hook
const useInterruptibleTimeout = (timeout, handler) => {
  const [elapsed, setElapsed] = useState(0);
  const start = React.useMemo(() => {
    return new Date().getTime();
  }, []);

  useEffect(() => {
    // console.log(elapsed);
    let timerId = -1
    if (timeout > elapsed) {
      timerId = setTimeout(function() {
        handler();
      }, timeout - elapsed);
    }

    return () => {
      setElapsed(new Date().getTime() - start);
      if (timerId > 0) clearTimeout(timerId);
    };
  }, [elapsed, handler, start, timeout]);


}

export function Home() {
  const [introEnded, setIntroEnded] = useState(false);

  // function to end intro animation
  const endIntro = React.useCallback(() => {
    console.log(introEnded); // THIS IS WRONG THE SECOND TIME FUNCTION IS CALLED
    if (!introEnded) {
      setIntroEnded(true);
      // do various other stuff
    }
    // do various other stuff
  }, [introEnded]);

  useInterruptibleTimeout(4000, endIntro)

  // user can skip intro on click
  return (
    <div>
      <div onClick={endIntro}>Skip Intro</div>
    </div>
  );
}

Надеюсь, этот код будет вам полезен

0 голосов
/ 06 января 2020

Некоторые советы, которые вы должны реализовать в своем коде:

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

import React, { useEffect, useState } from "react";

export function Home() {
  const [introEnded, setIntroEnded] = useState(false);

  // function to end intro animation
  function endIntro() {
    console.log(introEnded); // THIS IS WRONG THE SECOND TIME FUNCTION IS CALLED

    setIntroEnded(() => true);
    // do various other stuff
    // do various other stuff
  }

  // if user does not interact, intro ends automatically
  useEffect(() => {
    const setTimeoutId = setTimeout(() => {
      if (!introEnded) {
        endIntro();
      }
    }, 4000);

    return () => {
      clearTimeout(setTimeoutId);
    };
  }, []);

  // user can skip intro on click
  return (
    <div>
      <div onClick={() => endIntro()}>Skip Intro</div>
    </div>
  );
}

Вот пример: https://repl.it/@ali_master / reactSetTimeout

0 голосов
/ 06 января 2020

Вам не нужно вызывать всю функцию внутри useEffect, вы можете просто обновить свое вступление state.

Также вам может потребоваться вернуть функцию очистки, которая сбросит ваш state обратно до false в конце вашего useEffect, который будет имитировать componentWillUnmount(). Подробнее здесь уборка

const App = () => {
  const [introEnded, setIntroEnded] = React.useState(false);
  // function to end intro animation
  function endIntro() {
    setIntroEnded(true)
     // do your other stuff
     console.log('skipped')
  }

  // if user does not interact, intro ends automatically
  React.useEffect(() => {
    // Change state after x second
    setTimeout(function() {
      setIntroEnded(true)
    }, 4000);
    // Clean up and set it back to false
    return () => setIntroEnded(false)
  }, []);

  return (
    <div>
      <div onClick={endIntro}>Skip Intro</div>
      {!introEnded ? <div>Your Intro... This will end after 4 seconds..</div> : <div>Intro Ended... </div>} 
    </div>
  )

}

ReactDOM.render( <App /> , document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>

<div id="root"></div>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...