I made a React hook to get the current date
February 24, 2019 in JavaScriptReact Hooks are a fantastic new feature in React 16.8. It’s my favorite thing about React since React. Somehow the developers found a way to make side effects work in functional React components, with the plainest Javascript, and ten times less code than the class-based equivalent.
You can apply hooks to all kinds of use cases, but I had a quite simple one on my mind.
I’m working on an app where there’s a lot of business logic tied to the current date. Generally, when you need the current date in your app, you
do a new Date()
or a DateTime.local()
in Luxon,
which is what I’ve been doing so far. From a theoretical FP perspective, this is wrong because it makes your components (or worse, reducers) impure. From a
practical perspective, this is also wrong, because your components won’t redraw when the date changes: if you leave the app open and go to sleep, it’s going
to be stuck in yesterday until some other change forces the components to redraw. What could be even worse is if the date is inconsistent across the app, because
some components had a reason to change, but others did not.
I had planned to make a React context to distribute the current date through the application, and hooks presented an opportunity to do so with the cleanest possible API.
Provider
First of all, we declare our context. Nothing new here:
// CalendarDate is my custom class with `today()` and `equal(otherDate)` methods
const Today = React.createContext(CalendarDate.today());
Now, we build a provider that will take care of updating the context value.
Typically the provider would be a class component because it has state. (And in fact, I only use class components when I need state management.)
However, with hooks, we can make the provider a function and keep the code cleaner than a class component!
import { useState, useEffect } from "react";
export const TodayProvider: React.FunctionComponent<{}> = ({ children }) => {
// Allocate a state to keep the current date in
const [currentDate, setDate] = useState(CalendarDate.today());
// This function will be called every minute to check for changes
function updateDateIfChanged() {
const newDate = CalendarDate.today();
// setDate would always trigger a render in our case: dates are objects,
// and newDate will never be equal to currentDate if they are not the
// same instance.
// So, only setDate when we know that the date has changed meaningfully.
if (!newDate.equal(currentDate)) {
setDate(newDate);
}
}
useEffect(() => {
// Set up polling for date changes every minute
// (A sidenote about JS timers - they run on CPU time, not calendar time. This means that
// while the computer is in sleep mode, the timer is not ticking. That's why we need to
// check once a minute instead of calculating time to midnight and setting a precise alarm.)
const intervalID = setInterval(updateDateIfChanged, 60000);
// setInterval provides us with a cleanup function, pass that on
return () => clearInterval(intervalID);
// the effect is good as long as the component lives, no need to ever run it twice
}, []);
// Render the React context provider
return <Today.Provider value={currentDate}>{children}</Today.Provider>;
};
What I like about hooks is that they are simple single-purpose building blocks. You take them and assemble whatever business logic you need, with no boilerplate at all. Hooks are classic Javascript, just as I like it.
(What I don’t like is that hooks turn functional components impure and make testing harder. However, you can deal with that by injecting hooks with a HOC.)
Getting back to the example, first, we call a useState
hook to make some place to put the current date. That’s what the hook does - it gives you a persistent local variable and a setter for it.
(You might wonder - local and persisted to where? The code looks magical at first sight, but it’s not — the state is attached to the current instance of the component. That’s why hooks are only allowed within component code.)
Then we call a useEffect
hook. It attaches side effects to the component lifecycle. In this case, we use it to set up a polling mechanism with a familiar subscribe-cleanup idiom. (You return a cleanup function from the effect function if you need to.)
One thing you have to know about useEffect
is that it runs the cleanup and effect functions whenever the component is rendered, and not only when it’s mounted and unmounted. This is good for some side effects, but not for subscriptions.
To avoid unnecessary churn, we pass a second value to useEffect
, which is a cache key: the effect is only rerun when the key changes. In our case, the key is a never changing empty array: []
.
The typical subscription effect retrieves some data and puts it into a variable managed by useState
.
Consumer
Finally, there’s a useContext
hook that gives access to a context without having to wrap the component into a <Context.Consumer>
. We make a custom wrapper for the hook to hide the implementation details:
const useToday = () => useContext(Today);
export default useToday;
Now we have created our little hook (all functions that call hooks are technically hooks themselves, and the useSomething
name is a convention). Using it in a component could not be simpler:
const MyComponent: React.FunctionComponent<{}> = () => {
const today = useToday();
return <div>Today is: {today.toString()}</div>;
};
Now our component will be rerendered whenever the date changes.
As to using dates in reducers and other business logic — I recommend always passing the date as an argument. In my case, I get by with passing the date from UI components to actions and selectors.
Liked the post? Treat me to a coffee