useTime() React Hook
Countdowns in React are complicated/hard. Let’s use hooks to create a useTime hook.
The Hook (Code First!)
Copy-paste this gist (https://gist.github.com/jamesfulford/7f3311bd918982e68d911a9c70b27415) and give it a try in your codebase. Don’t worry, this article will still be here when you get back.
This gist currently uses Luxon for handling dates, but the comments show how to convert it to use Moment, or native JS Dates. An epoch integer is also a wise move if timezones are complicated.
PSA: If you’re working with timezones, use a library. Else, this will happen to you and all your friends: (I warned you)
Using the current time when rendering a React component can be tricky. The present literature (Medium articles, assorted blogs, and StackOverflow answers) often has 3 problems:
Implementation is Wrong
Working with the current time in React is hard. The common solution of
setInterval callbacks incrementing/decrementing state will fall out of sync quickly because of a misunderstanding of
state transitions in React.
Directly accessing the current time in the
render method will not trigger re-renders when needed, and passing through a prop/context only defers the problem to a different component. Therefore, a solution using state is the right solution.
Some implementations available on blogs, Medium articles, and StackOverFlow answers update state incorrectly. Some countdown implementations will store seconds remaining, then will decrement the seconds remaining every 1000ms using
setInterval. This will fall out of sync with actual-time very quickly, depending on:
- how long the tab is visible (browsers deprioritize
setIntervalcalls for non-focused pages for performance reasons)
- how complicated the rest of your app is (the thread may not get to the
setIntervalcallback quickly enough if there is a lot of work to do)
- the performance specs of the viewing device (mobile devices will execute JS slower, for example)
Getting React-specific, some implementations do not consider the fact that
this.setState will not immediately update state. This means the first state update may not have been executed before the second state update was queued. This is a problem if the implementation relies on the previous state to decide the next state, (i.e. time is incremented or decremented), because time will go out of sync (because two duplicate updates were queued).
The solution is either to pass a function to
this.setState as shown in the React docs, or (better solution) don’t rely on the previous state to get the current state. Inside the
setInterval callback, update state with the most recent time.
No Clean-Up is Done
In a class component, a
componentWillUnmount method can clean up the interval id using something like
clearInterval(this.state.intervalId). However, some implementations neglect this important detail, potentially causing memory leaks in the code of those who copy the implementation.
My Lifecycle Methods Are Huge (“It’s not a hook”)!
If a React component gets large (maybe you inherited bad code), it can take quite a lot of scrolling to go from the
setInterval call to the
clearInterval call and the accessing of the current time from
this.state. This violates one of my personal rules of thumb:
Things that are related should be close to each other.
Incidentally, this is the motivation for switching to hooks in React.
I have yet to find a hook that does this for consuming and updating the current time. So please, enjoy my implementation.
By the way, this “Things that are related should be close to each other” rule-of-thumb a special case of one of my personal favorite rules (which has life/moral implications if fully adopted):
Make it easy to do the right thing; make it hard to do the wrong thing.
To learn more about this philosophy, check out one of my earlier articles (hey, you read this far!):