How to build a DateCountdown in React?
Gabriel Linassi
Posted on May 26, 2022
Recently I applied to a position as a Frontend developer and they asked me to solve a React challenge. Among other things, they asked to build a reusable DateCountdown component. If you're interested to see the challenge, check this Reddit post.
Take a look at the Takeaways section at the end to learn some useful patterns I used here which can be useful in your projects.
TL;DR
This is how the component will look at the end. Check the sandbox for the full code.
const DateCountdown = ({ endDate }: DateCountdownProps) => {
const { rDays, rHours, rMinutes, rSeconds } = useDateCountdown(endDate);
return (
<div className={styles.countdown}>
<div className={styles.clock}>
<div className={styles.ticker}>{rDays}</div>
<div className={styles.label}>Days</div>
</div>
<Separator />
<div className={styles.clock}>
<div className={styles.ticker}>{rHours}</div>
<div className={styles.label}>Hours</div>
</div>
<Separator />
<div className={styles.clock}>
<div className={styles.ticker}>{rMinutes}</div>
<div className={styles.label}>Minutes</div>
</div>
<Separator />
<div className={styles.clock}>
<div className={styles.ticker}>{rSeconds}</div>
<div className={styles.label}>Seconds</div>
</div>
</div>
);
};
const endDate = new Date("2022-12-27T16:25:00");
() => <DateCountdown endDate={endDate} />
Implementation details π»
The code above is pretty much self explanatory so let's focus on the custom hook which is a little tricker.
import { useEffect, useState } from "react";
import { getRemainingTime } from "./DateCountdown.helpers";
type State = {
rDays: number;
rHours: number;
rMinutes: number;
rSeconds: number;
};
const initialState: State = { rDays: 0, rHours: 0, rMinutes: 0, rSeconds: 0 };
const useDateCountdown = (endDate: Date) => {
const [state, setState] = useState<State>(initialState);
useEffect(() => {
const remainingTime = getRemainingTime(endDate);
setState(remainingTime);
const intervalId = setInterval(() => {
const remainingTime = getRemainingTime(endDate);
setState(remainingTime);
}, 1000);
return () => clearInterval(intervalId);
}, [endDate]);
return state;
};
export { useDateCountdown };
Notice that I call the getRemainingTime
function outside of the setInterval
because the interval will call getRemainingTime
just after 1sec and I want it to start asap.
To implement the getRemainingTime
I added date-fns
as a dependency to help with the date calculations.
import {
getDaysInMonth,
getMonth,
getYear,
intervalToDuration
} from "date-fns";
export const getRemainingTime = (endDate: Date) => {
const now = new Date();
const difference = intervalToDuration({
start: now,
end: endDate
});
/**
* As the hook is not returning the number of months and years,
* it needs to aggreagate those values into the days counter
*/
let numOfDays = difference.days || 0;
let numOfMonths = (difference.months || 0) + (difference.years || 0) * 12;
for (let i = 1; i <= numOfMonths; i++) {
numOfDays += getDaysInMonth(new Date(getYear(now), getMonth(now) + i));
}
return {
rDays: numOfDays,
rHours: difference.hours || 0,
rMinutes: difference.minutes || 0,
rSeconds: difference.seconds || 0
};
};
Notice that the intervalToDuration
from date-fns
returns the range between seconds to years however my custom hook just returns the range between seconds to days so I had to implement the logic to convert the remaining months and years to days.
The logic is pretty much it. I used CSS Modules to style.
.countdown {
display: flex;
align-items: center;
}
.dots {
margin: 0 0.5rem;
}
.dot {
width: 3px;
height: 3px;
border-radius: 100%;
background-color: #fff;
}
.dot + .dot {
margin-top: 3px;
}
.clock {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
background-color: rgba(119, 126, 144, 0.2);
border-radius: 8px;
width: 70px;
height: 60px;
}
.ticker {
font-size: 1rem;
font-weight: 600;
}
.label {
font-size: 0.75rem;
}
@media only screen and (min-width: 640px) {
.dots {
margin: 0 1rem;
}
.dot {
width: 6px;
height: 6px;
}
.dot + .dot {
margin-top: 6px;
}
.clock {
width: 100px;
height: 90px;
}
.ticker {
font-size: 1.5rem;
font-weight: 600;
}
.label {
font-size: 1rem;
margin-top: 0.25rem;
}
}
Notes π
- At my original implementation here I didn't add the logic to convert months and years to days. I just realized that once I was writing this article π
Takeaways π―
Besides this code, I would like to call your attention to some good React patterns I assembled together here which can be useful to apply in other projects.
- Use custom hooks. They help you write clean code because it separates the view from the logic.
- Keep closed related files in the same folder. Notice that I nested some files inside of the DateCountdown folder because those are being used just by that component.
Sandbox
Posted on May 26, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.