24 October 2022
by
Hector Sosa

Build a Tiny Calendar without Flex or useState

calendar
day.js
tailwindcss
react
typescript
https://github.com/solarwindscloud/nighthawk-clients/pull/4157
Is it possible to build a fully functional calendar component under 7kB without using CSS Flex or useState? Let's explore that possibility using Day.js with CSS Grid, TailwindCSS, React, and TypeScript. Here's what each of these is bringing to the table today:
  • Day.js — a tiny and fast 2kB alternative API to parse, manipulate and display dates on the web (date-fns is 9.5 times larger).
  • TailwindCSS — skip CSS Flex by learning the fundamentals of CSS Grid the smart way using Tailwind's Grid utility classes.
  • React — extract state logic into reducers by exploring useReducer and skipping additional re-renders using useCallback
  • TypeScript — work smarter and faster by taking advantage of TypeScript's autocompletion and IntelliSense.
There's a lot of ground to cover, so please go through this guide along with the finished component: Calendar GH Repo | Open 'Calendar' in StackBlitz.

Getting started with Day.js

Regardless of the library of choice (if any), here's what we need: (a) current date, (b) current month, and (c) dates for the entire month. Let's take a look at how Day.js helps us to get started in defining those initial values:
1// USING THE LIBRARY `dayjs`
2// dayjs().toDate()      -> Timestamp of today's date / typeof Date
3// dayjs().daysinMonth() -> Days in today's month / typeof number
4
5// REUSABLE UTILITY FUNCTIONS FOR `dayjs`
6/** Create a date at the start of the day 00:00. */
7function today() {
8    return dayjs().startOf("day").toDate();
9}
10
11/** Create an array of Dates for a given month */
12function createMonth(month = today()) {
13    return Array.from(
14        { length: dayjs(month).daysInMonth() },
15        (n, i) => n = dayjs(month).date(i + 1).toDate()
16    );
17}
These functions will help us clearly define initial values for our calendar with very little code. Once these values are defined, we are ready to build our first Calendar grid to display each of those dates. The initial values are kept separately for our reducer function to use when we introduce our state logic into our component.
1const initialValues = {
2    selectedDate: today(),
3    currentMonth: today(),
4};
5
6export default function Calendar() {
7    const { selectedDate, currentMonth } = initialValues;
8    const currentMonthDates = createMonth(currentMonth);
9    return (
10        <div>
11            {currentMonthDates.map((date) => (
12                <div key={date.toString()}>
13                    {dat.toString()}
14                </div>
15            ))}
16        </div>
17    );
18}
Banner

Do you need a reliable partner in tech for your next project?

Building our Calendar using CSS Grid and TailwindCSS

Using TailwindCSS we can apply and define our Grid property with the utility class grid grid-cols-7, then we can map over our array of dates and create a button for each of them for our users to interact with. By applying the CSS property grid-template-columns: repeat(7, minmax(0, 1fr)); using grid-cols-7, we are explicitly defining the columns and allocation of our columns for all the rows of content to follow.
We also have an array named firstDayOfMonth that contains more Tailwind utility classes. We used this array to define a given utility class for our first item (set conditionally using index === 0) and start the calendar on the correct day of the week (i.e. Monday, Tuesday, etc.).
For any given date (i.e. 1st of October 2022), Day.js can tell us which day of the week that date falls in. For example, the 1st of October 2022 falls on a Saturday, so it's the 7th day of the week (based on a Sunday to Saturday calendar week), calling dayjs(date).day() will return 7 accessing the right utility class to display our calendar.
1export default function Calendar() {
2    // ...
3    const firstDayOfMonth = [
4        "col-start-1",
5        "col-start-2",
6        "col-start-3",
7        "col-start-4",
8        "col-start-5",
9        "col-start-6",
10        "col-start-7",
11    ];
12    return (
13        <div className="grid grid-cols-7">
14            {currentMonthDates.map((date, index) => (
15                <div
16                    className={index === 0 ? firstDayOfMonth[dayjs(date).day()] : ""}
17                    key={date.toString()}
18                >
19                    <button>{dayjs(date).format("D")}</button>
20                </div>
21            ))}
22        </div>
23    );
24}

Using Reducers to Manage State

Now we need to think about how to further reduce complexity, keeping all of our calendar logic in a single easy-to-access place using reducers.
„Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a reducer.“
Reducers are a great way to cut down on code when many event handlers modify state in a similar or related way (i.e. when updating a month, we also need to update the days of the month). It also helps you cleanly separate your state logic and improve readability to easily understand what happened on each update.
We need to (1) write a reducer function (which will process all of our actions), (2) use the reducer (function and initial values) in our component, and (3) set dispatch actions to update our component.
1// REDUCER FUNCTION OUTSIDE COMPONENT
2/** Manages state for selected date and current month  */
3function reducer(state, action) {
4    switch (action.type) {
5        case "SELECT_DATE": {
6            return {
7                ...state,
8                selectedDate: action.value,
9            };
10        }
11        case "UPDATE_MONTH": {
12            return {
13                ...state,
14                currentMonth: action.value,
15            };
16        }
17    }
18}
19
20// USING OUR REDUCER IN COMPONENT
21export default function Calendar() {
22    const [ state, dispatch ] = useReducer(reducer, initialValues);
23    // ...
24    return (
25        // ...
26    );
27}
28
29// DISPATCHING AN ACTION WITHIN COMPONENT
30/** Performs calculations and dispatches reducer actions to update state */
31function handleDispatch(action) {
32    const { type, value } = action;
33    switch (type) {
34        case "SELECT_DATE": {
35            dispatch({ type, value });
36            // TODO: Return component's value
37            // setValue: value;
38            break;
39        }
40        case "UPDATE_MONTH": {
41            const updatedMonth = dayjs(currentMonth)
42                .add(value, "month")
43                .toDate();
44            dispatch({ type, value: updatedMonth });
45            break;
46        }
47    }
48}

Avoiding Additional Renders

„When you optimize rending performance, you will sometimes need to cache the functions that you pass to child components.“
Once our calendar is looking good and behaving the way we want it to, we need to figure out a way for our Calendar to return a Date value for our application. To avoid re-rendering our component, we can use a combination of the ref property and React's useCallback.
React uses ref as a reserved property on built-in primitives, where it stores DOM nodes once a component is rendered/mounted. However, the ref's type declaration type Ref<T> = RefCallback<T> | RefObject<T> | null not only allows a ref object into it, but also a callback function. This allows us to cache a function definition (using both ref and useCallback) without having to use useEffect. Let's implement a callback ref for our calendar component:
1export default function Calendar({value, setValue}) {
2    /** Cached function to update component's state at load */
3    const callbackRef = useCallback(() => {
4        setValue(selectedDate);
5    }, []);
6    /** Performs calculations and dispatches reducer actions to update state */
7    function handleDispatch(action) {
8        const { type, value } = action;
9        switch (type) {
10            case "SELECT_DATE": {
11                dispatch({ type, value });
12                setValue(value);
13                break;
14            }
15            // ...
16        }
17    }
18    // ...
19    return (
20        <div ref={callbackRef}>
21            // ...
22        </div>
23    );
24}
useCallback will return a memoized version of the callback that only changes if one of the inputs has changed.“
For a more detailed explanation, Tk's post has a more detailed explanation of this structure: Avoiding useEffect with Callback refs.

Nice things to add

After setting up our data, styling our calendar, and managing its state, here are a couple of nice things that we added to our component:
  • Refactor the component into a "container/presentational" pattern. Use the "container" component to work with the data/state and then pass the data as props to the "presentational" component to display its UI.
  • Use an array and map it to create labels for the days of the week: dayjs().day(index).format("dd").
  • Add a function to generate dynamic TailwindCSS classes using the native Boolean function: function classNames(...classes) { return classes.filter(Boolean).join(" ")}.
  • Use focusable elements <button /> and descriptive labels aria-label to make sure your component is accessible.
Feel free to explore this example using the resources below:
Share post

Let’s stay connected

Do you want the latest and greatest from our blog straight to your inbox? Chuck us your email address and get informed.
You can unsubscribe any time. For more details, review our privacy policy