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
Top comments (0)