DEV Community

Antoine Quinquenel for Wecasa

Posted on

How to schedule an update of a React component in the future

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 use useReducer instead of useState to not bother passing params to the state setter, but naturally useState 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 for useRerenderTimeout, we use useReducer instead of useState to not bother
passing params to the state setter, but naturally useState 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. 

TL;DR

useRerenderTimeout hook

 

Use case:

 

useRerenderInterval hook

 

Use case:

 

Top comments (0)