Недавно я подал заявку на должность Frontend-разработчика, и они попросили меня решить задачу на React. Среди прочего, они попросили создать многократно используемый компонент DateCountdown. Если вам интересно ознакомиться с задачей, посмотрите этот пост на Reddit.
Загляните в раздел Takeaways в конце, чтобы узнать о некоторых полезных паттернах, которые я использовал здесь и которые могут пригодиться в ваших проектах.
TL;DR
Вот как компонент будет выглядеть в конце. Проверьте песочницу для полного кода.
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} />
Детали реализации 💻
Приведенный выше код практически не требует объяснений, поэтому давайте сосредоточимся на пользовательском хуке, который немного сложнее.
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 };
Обратите внимание, что я вызываю функцию getRemainingTime
вне setInterval
, потому что интервал вызовет getRemainingTime
сразу после 1 секунды, а я хочу, чтобы это началось как можно скорее.
Для реализации getRemainingTime
я добавил date-fns
в качестве зависимости, чтобы помочь с вычислением даты.
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
};
};
Обратите внимание, что intervalToDuration
из date-fns
возвращает диапазон от секунд до лет, однако мой пользовательский хук возвращает только диапазон от секунд до дней, поэтому мне пришлось реализовать логику для преобразования оставшихся месяцев и лет в дни.
Логика в основном такова. Для стилизации я использовал модули CSS.
.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;
}
}
Примечания 📖
- В моей первоначальной реализации я не добавил логику для преобразования месяцев и лет в дни. Я только что понял это, когда писал эту статью 🙈.
Выводы 🎯
Помимо этого кода, я хотел бы обратить ваше внимание на несколько хороших паттернов React, которые я собрал здесь, и которые могут быть полезны для применения в других проектах.
- Используйте пользовательские хуки. Они помогают писать чистый код, потому что отделяют представление от логики.
- Храните закрытые связанные файлы в одной папке. Обратите внимание, что я вложил некоторые файлы внутрь папки DateCountdown, потому что они используются только этим компонентом.
Песочница