admin管理员组

文章数量:1422504

I'm trying to make a countdown timer using React-Native, React Hooks, MomentJS and (setTimeout/setInterval). Whatever approach I try to use, it fails. The problem is that ponent is never re-rendered.

I tried to follow the official React Hooks documentation, a few articles on Medium, for example The Iceberg of React Hooks but nothing works.

One possibility is that it needs deep clone of the MomentJS object, but it's an inefficient approach I guess.

This is one of the reproducible examples that I've tried.

const Timer = () => {
  const [time, setTime] = useState(moment.duration(30, 'seconds'))
  const intervalRef = useRef()

  useEffect(() => {
    intervalRef.current = setTimeout(() => {
      setTime(prevTime => prevTime.subtract(1, 'second'))
    }, 1000)

    return () => {
      clearInterval(intervalRef.current)
      intervalRef.current = null
    }
  })

  return (
    <View>
      {time.asSeconds()}
    </View>
  )

I'm trying to make a countdown timer using React-Native, React Hooks, MomentJS and (setTimeout/setInterval). Whatever approach I try to use, it fails. The problem is that ponent is never re-rendered.

I tried to follow the official React Hooks documentation, a few articles on Medium, for example The Iceberg of React Hooks but nothing works.

One possibility is that it needs deep clone of the MomentJS object, but it's an inefficient approach I guess.

This is one of the reproducible examples that I've tried.

const Timer = () => {
  const [time, setTime] = useState(moment.duration(30, 'seconds'))
  const intervalRef = useRef()

  useEffect(() => {
    intervalRef.current = setTimeout(() => {
      setTime(prevTime => prevTime.subtract(1, 'second'))
    }, 1000)

    return () => {
      clearInterval(intervalRef.current)
      intervalRef.current = null
    }
  })

  return (
    <View>
      {time.asSeconds()}
    </View>
  )
Share Improve this question asked Aug 30, 2019 at 14:48 Honza SedloňHonza Sedloň 3741 gold badge11 silver badges28 bronze badges 4
  • Mutating the object isn't what you should with set*() (ad absurdum - you do not even need to call setTime() in that case). To avoid re-rendering when state/properties do not change is one of those basic optimization that makes React easy to use. To create a new duration object is a negligible overhead and you can quickly do it using clone(). Alternatively you might consider to use number instead of moment.Duration and to create a new object when rendering. Also note that you do not need intervalRef, you can save const ref = setTimeout(...); return () => clearInterval(ref); – Adriano Repetti Commented Aug 30, 2019 at 15:06
  • It's odd. I always thought that in order to not-rerender a ponent when the state did changed but the object was identical you had to implement it as a pure ponent – apokryfos Commented Aug 30, 2019 at 15:33
  • the state hasn't actually changed though, the reference to the object is the same – Will Jenkins Commented Aug 30, 2019 at 15:45
  • @WillJenkins in non-functional ponents calling setState always rerenders. Thats why I was confused, though now that I think about it set state probably always clones the state – apokryfos Commented Aug 30, 2019 at 15:49
Add a ment  | 

2 Answers 2

Reset to default 6

You're correct, it isn't re-rendering because your moment object is the same (but mutated) on every tick. You can easily get it working by adding .clone() in your setTime updater:

const Timer = () => {
  const [time, setTime] = useState(moment.duration(30, "seconds"));
  const intervalRef = useRef();

  useEffect(() => {
    intervalRef.current = setTimeout(() => {
      setTime(prevTime => prevTime.clone().subtract(1, 'second'))
    }, 1000)

    return () => {
      clearInterval(intervalRef.current)
      intervalRef.current = null
    }
  })

  return <div>{time.asSeconds()}</div>;
};

Working sandbox here: https://codesandbox.io/s/gifted-euler-e8xg5

One possibility is that it needs deep clone of the MomentJS object, but it's an inefficient approach I guess.

Yes exactly. React doesn't rerender if the current and the previous state equal. You could just store the seconds in the state.

And you don't need that ref.

const Timer = () => {
  const [time, setTime] = useState(30 /*s*/)

  useEffect(() => {
    const timeout = setTimeout(() => {
      setTime(prevTime => prevTime - 1);
    }, 1000)

    return () => clearTimeout(timeout);
  }, [time])

  return (
   <View>
     {time}
   </View>
 );

本文标签: