useRerenderTimeout
& useRerenderInterval
: 2 custom hooks to re-render a React component at a specific time, or at a regular interval of time.
⚡️ In a hurry ? Go directly to the TL;DR section.
Most of the time, we develop user interfaces (UI) that react to user interactions.
But what if you need the UI to react to the current time ?
At Wecasa, we recently needed some content of our React Native app to update automatically at a specific time.
To do so, we created two custom hooks that allow a component to re-render itself at a specific time or at a regular interval of time, with a simple and generic implementation for a great developer experience. ✨
Trigger a re-render at a specific time
Let's say we want to create a Meeting
component that renders differently depending on whether the meeting has started or not.
Currently, nothing forces the component to change once the meeting starts. This means, if it is displayed at 13:59 and the meeting starts at 14:00, after one minute it will continue showing the meeting as upcoming even though it has started.
To force a re-render of the component, we can simply trigger a state update with a local timer that executes at the meeting's start time.
The timer is set via a custom
useTimeout
hook, inspired by this article from Josh Comeau. It's also available in popular hooks libraries, such as usehooks-ts or @uidotdev/usehooks.
We useuseReducer
instead ofuseState
to not bother passing params to the state setter, but naturallyuseState
works as well.
Now, our
Meeting
component already refreshes itself at the meeting's start time automatically! 🎉But there is still room for improvement, especially in terms of performance ⚡️ and readability 🤓
Optimizing the execution
Let’s say the meeting starts at 14:00:00, and the Meeting
component first renders one minute earlier, at 13:59:00.
Now, if the Meeting
component re-renders at 13:59:30 for any reason (for instance, if one of its parent components re-renders), delay
will be recalculated, changing from 60 seconds to 30 seconds.
As a result, useTimeout
will destroy the current timer and create a new one that will still execute at the same time, 14:00:00, since startsAt
remains unchanged.
To prevent this unnecessary "clean-up", we can memoize delay
so that it only changes when startsAt
is updated.
Also, we don’t want to set a timer if the meeting has already started, so in that case, we set delay
to null
.
Now, our
Meeting
component will set a single timer throughout its entire lifecycle(as long as
startsAt
remains unchanged), and only if the meeting has not yet started.Encapsulate into a custom hook
Our Meeting
component is becoming difficult to read, and we'd like to extract this logic into a reusable piece of code: we can create a custom useRerenderTimeout
hook that takes the date at which the component should re-render as an argument.
Since the
date
parameter is a dependency of this useMemo
, we want to ensure its reference remains stable across renders. For this reason, we define it as a string
rather than a Date
object.
Then, the Meeting
component would look like this:
By moving the logic into a custom hook, the
Meeting
component only needs a single line of code to re-render itself at the desired time. ✨This keeps the code light and clear, and offers a generic solution that's easy to use.
Trigger multiple re-renders at specific times
Let's say we now want to display a progress bar indicating the remaining time, starting 30 minutes before the meeting begins.
The Meeting
component will now have a third possible status," imminent", in addition to "upcoming" and "started".
This means the component needs to re-render twice: once 30 minutes before the meeting starts, and once when the meeting starts.
To improve code cohesion, we can associate each status with the time at which it ends, which is also the time we want the Meeting
component to re-render.
When
Meeting
mounts, it will set a timeout to trigger a re-render 30 minutes before the meeting starts.Once this timeout executes,
getMeetingStatus
will be called again, returning a new value for status.endsAt
, which will set a new timeout to trigger a re-render at the meeting start time.This "call loop", based on the component's lifecycle, allows us to handle both re-renders using a single instance of the
useRerenderTimeout
hook. 🎉Trigger a re-render at a regular interval of time
The "imminent" status lasts 30 minutes, but we want to avoid running a 30-minute-long animation for the progress bar.
Instead, we refresh the progress bar at 1% increments, which means updating it every 18 seconds.
While we could use the same "call loop" approach with useRerenderTimeout
, it's simpler in this case to use an interval instead of a timeout, as the time between re-renders is constant.
We can create a similar hook to useRerenderTimeout
that runs an interval instead of a timeout.
The interval is set via a custom
useInterval
hook, inspired by
this article from Dan Abramov.
Just like we did foruseRerenderTimeout
, we useuseReducer
instead ofuseState
to not bother
passing params to the state setter, but naturallyuseState
works as well.
The interval continues as long as
ms
is a positive value, and it stops when ms
is set to null
.Depending on the faith you have in yourself, you may want to add a safeguard to prevent unexpected excessively frequent intervals (recommended).
For example, we chose an arbitrary minimum interval of 1 second.
The
ProgressBar
component can then use this hook to re-render itself every 18 seconds, independently of the Meeting
component:Using this hook directly in the
Progress
component instead of its parent ensures that the minimum amount of JSX is updated.
In general, these hooks should be used as low as possible in the component tree.
Every 18 seconds, useRerenderInterval
triggers a re-render,
recalculating remainingPercentage
and automatically updating the relevant UI. ✅
Conclusion
Managing timers and intervals in React components can be tricky, and can quickly make the code hard to read and to maintain.
Creating these two hooks useRerenderTimeout
and useRerenderInterval
allowed us to easily implement complicated business logics based on timers and intervals in just a few lines of code.
They take advantage of the component's lifecycle, by triggering re-renders via local state updates, to keep the rest of the component's logic decoupled from its need to refresh at specific times, in a simple and generic way for a great developer experience.
Top comments (0)