React中onClick内的状态引用是错误的 – 可能是闭包问题?

huangapple go评论90阅读模式
英文:

React state reference within onClick is wrong - possible closure issue?

问题

以下是翻译好的内容:

这是一个简化版本的React组件示例。它是一个主页屏幕,具有介绍动画,该动画将在一段时间后自动结束并淡出,但用户也可以跳过它。

问题是,用户跳过后,当超时调用endIntro()第二次时,introEnded状态出现了问题。我明确在第一次调用endIntro()时将其设置为true,但第二次调用endIntro()似乎引用了旧状态。

研究告诉我这是一种闭包问题,但我无法找出解决方法。

发生了什么,我该如何解决?谢谢!

英文:

Here is a stripped down version of a React component that I have. It is a home screen with an intro animation, that will automatically end and fade out after a timeout, but can also be skipped by the user.

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>
  );
}

The problem is, after a user skips, when the timeout kicks in and calls endIntro() a second time, the introEnded state is wrong. I have clearly set it to true the first time endIntro() was called, yet the second call to endIntro() seems to reference an older state.

Research tells me it's some sort of closure issue, but I can't work out the solution.

What's going on, and how do I fix this? Thanks!

答案1

得分: 1

是的,这是一个JavaScript闭包问题。当调用useEffect时,变量introEnded的值为false,而这是无法避免的。为了实现这个目标,我们需要编写一些手动的代码,如下所示:

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

  // 结束介绍动画的函数
  const endIntro = React.useCallback(() => {
    console.log(introEnded); // 第二次调用函数时,这里是错误的
    if (!introEnded) {
      setIntroEnded(true);
      // 做各种其他事情
    }
    // 做各种其他事情
  }, [introEnded]);

  // 如果用户没有交互,介绍将自动结束
  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]);

  // 用户可以点击跳过介绍
  return (
    <div>
      <div onClick={endIntro}>跳过介绍</div>
    </div>
  );
}

我认为这是一般的用例,我们可以编写我们自己的自定义钩子。

// 自定义钩子
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);

  // 结束介绍动画的函数
  const endIntro = React.useCallback(() => {
    console.log(introEnded); // 第二次调用函数时,这里是错误的
    if (!introEnded) {
      setIntroEnded(true);
      // 做各种其他事情
    }
    // 做各种其他事情
  }, [introEnded]);

  useInterruptibleTimeout(4000, endIntro)

  // 用户可以点击跳过介绍
  return (
    <div>
      <div onClick={endIntro}>跳过介绍</div>
    </div>
  );
}

希望这段代码对你有所帮助。

英文:

Yes, this is a Javascript closure issue. the value of variable introEnded is false when useEffect is called and this's not avoidable. In order to achieve this goal, we need to write some manual code like below.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

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

  // function to end intro animation
  const endIntro = React.useCallback(() =&gt; {
    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(() =&gt; {
    // console.log(elapsed);
    let timerId = -1
    if (4000 &gt; elapsed) {
      timerId = setTimeout(function() {
        endIntro();
      }, 4000 - elapsed);
    }

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

  // user can skip intro on click
  return (
    &lt;div&gt;
      &lt;div onClick={endIntro}&gt;Skip Intro&lt;/div&gt;
    &lt;/div&gt;
  );
}

<!-- end snippet -->

I think this is the general use case and we can write our custom hook.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

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

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

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


}

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

  // function to end intro animation
  const endIntro = React.useCallback(() =&gt; {
    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 (
    &lt;div&gt;
      &lt;div onClick={endIntro}&gt;Skip Intro&lt;/div&gt;
    &lt;/div&gt;
  );
}

<!-- end snippet -->

I hope this code helpful to you

答案2

得分: 0

你不必在useEffect中调用整个函数,只需更新你的intro状态。

另外,你可能需要返回一个清理函数,该函数将在useEffect结束时将你的状态重置为false,以模拟componentWillUnmount()。在这里阅读更多清理的信息。

const App = () => {
  const [introEnded, setIntroEnded] = React.useState(false);

  // 结束介绍动画的函数
  function endIntro() {
    setIntroEnded(true);
    // 进行其他操作
    console.log('skipped');
  }

  // 如果用户没有交互,介绍会自动结束
  React.useEffect(() => {
    // x秒后更改状态
    setTimeout(function() {
      setIntroEnded(true);
    }, 4000);

    // 清理并将状态设置回false
    return () => setIntroEnded(false);
  }, []);

  return (
    <div>
      <div onClick={endIntro}>跳过介绍</div>
      {!introEnded ? <div>您的介绍... 这将在4秒后结束...</div> : <div>介绍已结束...</div>} 
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

这是你的代码的翻译部分。

英文:

You don't have to call the entire function inside useEffect you can just update your intro state.

Also you might need to return a cleanup function that will reset your state back to false at the end of your useEffect which will simulate the componentWillUnmount(). Read more here cleaning up

<!-- begin snippet: js hide: false console: true babel: true -->

<!-- language: lang-js -->

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

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

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

}

ReactDOM.render( &lt;App /&gt; , document.getElementById(&#39;root&#39;));

<!-- language: lang-html -->

<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>

&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;

<!-- end snippet -->

答案3

得分: -1

以下是您需要在代码中实现的一些建议:

  1. 在Home组件销毁时,通过返回的ID清除超时。
  2. 使用setState回调函数,并在调用setTimeout回调函数之前进行检查。

以下是代码部分:

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

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

  // 结束介绍动画的函数
  function endIntro() {
    console.log(introEnded); // 第二次调用函数时,这里是错误的

    setIntroEnded(() => true);
    // 执行其他各种操作
    // 执行其他各种操作
  }

  // 如果用户没有交互,介绍将在一定时间后自动结束
  useEffect(() => {
    const setTimeoutId = setTimeout(() => {
      if (!introEnded) {
        endIntro();
      }
    }, 4000);

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

  // 用户可以点击跳过介绍
  return (
    <div>
      <div onClick={() => endIntro()}>跳过介绍</div>
    </div>
  );
}

您可以在此示例中查看代码:https://repl.it/@ali_master/reactSetTimeout

英文:

Some tips you have to implement in your code:

  1. clear timeout by returned id when the Home component destroyed.
  2. using setState callback instead and checking before calling the function into setTimeout callback.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

import React, { useEffect, useState } from &quot;react&quot;;

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(() =&gt; true);
    // do various other stuff
    // do various other stuff
  }

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

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

  // user can skip intro on click
  return (
    &lt;div&gt;
      &lt;div onClick={() =&gt; endIntro()}&gt;Skip Intro&lt;/div&gt;
    &lt;/div&gt;
  );
}

<!-- end snippet -->

Here is an example: https://repl.it/@ali_master/reactSetTimeout

huangapple
  • 本文由 发表于 2020年1月6日 18:47:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/59610713.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定