Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setInterval 在 useEffect 中 的 state 未更新 #4425

Closed
Songkeys opened this issue Sep 8, 2019 · 7 comments
Closed

setInterval 在 useEffect 中 的 state 未更新 #4425

Songkeys opened this issue Sep 8, 2019 · 7 comments
Labels
question Further information is requested

Comments

@Songkeys
Copy link
Contributor

Songkeys commented Sep 8, 2019

import Taro, { useState, useEffect } from '@tarojs/taro'
import { View } from '@tarojs/components'

export default function Loading(props) {
  const [ellipsis, setEllipsis] = useState('.')

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(ellipsis)
      if (ellipsis.length === 6) {
        setEllipsis('.')
      } else {
        setEllipsis(val => val + '.')
      }
    }, 500)

    return () => {
      clearInterval(timer)
    }
  }, [])

  return (
    <View className="container">
      <View className="hint">{props.title + ellipsis}</View>
    </View>
  )
}

Loading.defaultProps = {
  title: '疯狂加载中',
}

效果是视图上会每 500ms 在省略号上多加一个 .,但是 js 里 ellipsis 永远是 .,并没有更新到最新状态。

@taro-bot
Copy link

taro-bot bot commented Sep 8, 2019

欢迎提交 Issue~

如果你提交的是 bug 报告,请务必遵循 Issue 模板的规范,尽量用简洁的语言描述你的问题,最好能提供一个稳定简单的复现。🙏🙏🙏

如果你的信息提供过于模糊或不足,或者已经其他 issue 已经存在相关内容,你的 issue 有可能会被关闭。

Good luck and happy coding~

@renshengwudi
Copy link

定时器 在hooks 不是这样用的,可以查找 react hooks setInterval

@Songkeys
Copy link
Contributor Author

Songkeys commented Sep 9, 2019

@renshengwudi 非常感谢。

我去搜了 Dan 的这篇文章:https://overreacted.io/making-setinterval-declarative-with-react-hooks/#second-attempt

里面就提到了这个常犯的错误,是由于闭包引起的:

例子:

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

这里的 count 会一直是 1。因为 setInterval 会拿到一个过期的 count(称作 "stale state")。原话解释:

The problem is that useEffect captures the count from the first render. It is equal to 0. We never re-apply the effect so the closure in setInterval always references the count from the first render, and count + 1 is always 1.

(但我还不是很能理解为什么这里会由于闭包呢?是和 setCount 还是 useEffect 的内部实现有关系吗?)

解决方法之一,是把 setCount(count + 1) 改成 setCount(c => c + 1)。这样可以确保一直拿到的是新鲜的 count(称作 "fresh state")。

但是我在 Taro 中使用碰到的上述问题里,确实是使用了这种 setCount(c => c + 1) 的方式呀。而且即便 useEffect 会因为闭包而拿到 stale state,jsx 中也不应该有不同的表现。 这是不是 Taro 的一个 bug 呢? @luckyadam


然后是关于 setInterval 在 React 中究竟该咋用的一些讨论——

React 官方文档里用了 setInterval 为例子来解释 useRef() 的作用:https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables 。但我不确定这是不是意味着 setInterval 最好就这么用。

Dan 的那篇文章里也是用了 useRef() 来实现,而且还自己包装了一个 useInterval()
https://overreacted.io/making-setinterval-declarative-with-react-hooks/#just-show-me-the-code ,具体代码:

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

但在我看来,这也太「过度实现」了。之前有生命周期的 class 写法,反而更直观。或许也还是我思维还没转变过来,useEffect(() => {}, []) 并不能简单地当成是 componentDidMountcomponentWillUnmount 的替代。

那么咋办呢。最后,我还是选择了用 setTimeout 来代替实现,避免使用 setInterval。就像 Ryan 在 React Conf 2018 上实现的那样。(https://youtu.be/dpw9EHDh2bM?t=4537

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setTimeout(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearTimeout(id);
  }, [count]);

  return <h1>{count}</h1>;
}

@Songkeys Songkeys changed the title hooks 在 useEffect 中 的 state 未更新 setInterval 在 useEffect 中 的 state 未更新 Sep 9, 2019
@Chen-jj
Copy link
Contributor

Chen-jj commented Sep 10, 2019

@Songkeys

(但我还不是很能理解为什么这里会由于闭包呢?是和 setCount 还是 useEffect 的内部实现有关系吗?)

并不是什么内部实现的问题,理解闭包的基本原理就可以理解,函数在哪里定义,就从此作用域开始往上寻找引用的变量而已。

但是我在 Taro 中使用碰到的上述问题里,确实是使用了这种 setCount(c => c + 1) 的方式呀。而且即便 useEffect 会因为闭包而拿到 stale state,jsx 中也不应该有不同的表现。 这是不是 Taro 的一个 bug 呢?

你 setCount 用法是对的,但你直接用 ellipsis 进行判断: if (ellipsis.length === 6) ,根据闭包可得 ellipsis 每次渲染都会为 '.'。

解决办法很简单,用 useRef 保存最新的 ellipsis,判断时取出来判断。

function Index() {
  const [ellipsis, setEllipsis] = useState('.')
  const ellRef = useRef()
  ellRef.current = ellipsis

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(ellRef.current)
      if (ellRef.current.length === 6) {
        setEllipsis('.')
      } else {
        setEllipsis(val => val + '.')
      }
    }, 1000)

    return () => {
      clearInterval(timer)
    }
  }, [])

  return (
    <div>
      <div>{ellipsis}</div>
    </div>
  )
}

@Chen-jj Chen-jj added answered question Further information is requested labels Sep 10, 2019
@taro-bot
Copy link

taro-bot bot commented Sep 10, 2019

Hello~

您的问题楼上已经有了确切的回答,如果没有更多的问题这个 issue 将在 15 天后被自动关闭。

如果您在这 15 天中更新更多信息自动关闭的流程会自动取消,如有其他问题也可以发起新的 Issue。

Good luck and happy coding~

@andyyxw
Copy link

andyyxw commented Apr 28, 2020

你 setCount 用法是对的,但你直接用 ellipsis 进行判断: if (ellipsis.length === 6) ,根据闭包可得 ellipsis 每次渲染都会为 '.'。

解决办法很简单,用 useRef 保存最新的 ellipsis,判断时取出来判断。

@Chen-jj @Songkeys 的确是由于useEffect没有ellipsis状态的依赖,导致useEffect只执行了一次,并且不会执行clearInterval,所以程序一直在跑。
那么直接在useEffect的依赖里加上ellipsis不就解决了吗。虽然useRef也能解决问题但是我觉得完全没必要啊,还是有其他场景我没考虑到呢。

@andyyxw
Copy link

andyyxw commented Apr 28, 2020

你 setCount 用法是对的,但你直接用 ellipsis 进行判断: if (ellipsis.length === 6) ,根据闭包可得 ellipsis 每次渲染都会为 '.'。
解决办法很简单,用 useRef 保存最新的 ellipsis,判断时取出来判断。

@Chen-jj @Songkeys 的确是由于useEffect没有ellipsis状态的依赖,导致useEffect只执行了一次,并且不会执行clearInterval,所以程序一直在跑。
那么直接在useEffect的依赖里加上ellipsis不就解决了吗。虽然useRef也能解决问题但是我觉得完全没必要啊,还是有其他场景我没考虑到呢。

sorry,反复阅读了上面提到的dan的那篇文章,终于理解了:

When we run clearInterval and setInterval, their timing shifts. If we re-render and re-apply effects too often, the interval never gets a chance to fire!
We can see the bug by re-rendering our component within a smaller interval:
https://codesandbox.io/s/9j86r218y4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants