DEV Community

Cover image for How to build a DateCountdown in React?
Gabriel Linassi
Gabriel Linassi

Posted on

How to build a DateCountdown in React?

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>
  );
}; 
Enter fullscreen mode Exit fullscreen mode
const endDate = new Date("2022-12-27T16:25:00");
() => <DateCountdown endDate={endDate} />
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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
  };
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)