Taro: setInterval 在 useEffect 中 的 state 未更新

Created on 8 Sep 2019  ·  7Comments  ·  Source: NervJS/taro

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 永远是 .,并没有更新到最新状态。

question

Most helpful comment

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

All 7 comments

欢迎提交 Issue~

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

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

Good luck and happy coding~

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

@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

(但我还不是很能理解为什么这里会由于闭包呢?是和 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>
  )
}

Hello~

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

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

Good luck and happy coding~

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

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

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

你 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

Was this page helpful?
0 / 5 - 0 ratings