Yet another datepicker in React

06/18/20192 Min Read — In React, Datepicker


Yet another datepicker in React, ha? But why, if you can use react-dates. It is a great library if you already use moment, which is a widely used library for date and time manipulations. However, the modern web is pushing us to keep reducing bundle sizes, and that is a problem yet to be addressed by the creators of moment.

Long story short: I decided to create my own datepicker.

Obviously bundle size was not the sole motivation. I noticed that the most used libraries haven't adopted css-in-js and react hooks or a more functional programing approach.

To sum up:

Motivation

  • Support native JavaScript date, instead of the moment object,
  • RTL support,
  • internationalize,
  • mobile-friendly,
  • selecting a specific date or range
  • theming with styled-system
  • written in TypeScript
  • React hooks
  • etc.

Check out the storybook examples or keep on reading for the technical details.

Architecture

Still with me? Let's get to business.

I wanted the datepicker to be used in a variety of scenarios, so I split the project into two libraries @datepicker-react/hooks and @datepicker-react/styled.

@datepicker-react/hooks

@datepicker-react/hooks are React hooks (if you are not familiar with React hooks, check out the official documentation), that make it easy to build a datepicker or calendar component, tailored to your needs in a few minutes. The library contains three hooks, namely useDatepicker, useMonth and useDay.

Install

yarn add @datepicker-react/hooks

useDatepicker

This hook the core of @datepicker-react/hooks. It manages the datepicker logic. You can control the selected dates using the startDate, endDate, and onDatesChange props as shown below.

const {
activeMonths,
isDateSelected,
isFirstOrLastSelectedDate,
isDateHovered,
firstDayOfWeek,
onDateSelect,
onResetDates,
goToPreviousMonths,
goToNextMonths,
numberOfMonths,
hoveredDate,
onDateHover,
isDateFocused,
focusedDate,
onDateFocus,
isDateBlocked,
} = useDatepicker({
startDate: new Date(2019, 1, 1),
endDate: new Date(2019, 1, 10),
focusedInput: null,
onDatesChange: (props) => {
console.log(props)
},
})

useMonth

useMonth returns an array of days, weekDayLabels and monthLabel, depends on the year and month props.

const {
days,
weekdayLabels,
monthLabel,
} = useMonth({
year: 2019,
month: 1,
})

useDay

Returns callbacks and flags used for styling.

const {
tabIndex,
isSelected,
isSelectedStartOrEnd,
isWithinHoverRange,
disabledDate,
onKeyDown,
onClick,
onMouseEnter,
} = useMonth({
date,
focusedDate,
isDateSelected,
isDateFocused,
isFirstOrLastSelectedDate,
isDateHovered,
isDateBlocked,
onDateSelect,
onDateFocus,
onDateHover,
dayRef,
})

@datepicker-react/styled

This part of the library is used for styling. As mentioned above it leverages css-in-js. There are quite a few great libraries to choose from, but I decided to pick styled-components, since it's the one I am most comfortable with.

The result is an easily internationalizable, accessible, mobile-friendly datepicker library for the web. It supports RTL and theming with styled-system.

The library can be used as a single date input (DateSingleInput), a input range (DateRangeInput) or as a "classical" datepicker component (Datepicker)

Install

yarn add @datepicker-react/styled styled-components

Include components

import {DateRangeInput, DateSingleInput, Datepicker} from '@datepicker-react/styled'

DateRangeInput example

The DateRangeInput is a fully controlled component that allows users to select a date range. You can control the selected dates using the startDate, endDate, and onDatesChange props as shown below. Similarly, you can control which input is focused as well as calendar visibility (the calendar is only visible if focusedInput is defined) with the focusedInput and onFocusChange props as shown below.

Here is the minimum REQUIRED setup you need to get the DateRangeInput working:

import React, {useReducer} from 'react'
import {DateRangeInput} from '@datepicker-react/styled'
const initialState = {
startDate: null,
endDate: null,
focusedInput: null,
}
function reducer(state, action) {
switch (action.type) {
case 'focusChange':
return {...state, focusedInput: action.payload}
case 'dateChange':
return action.payload
default:
throw new Error()
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<DateRangeInput
onDatesChange={data => dispatch({type: 'dateChange', payload: data})}
onFocusChange={focusedInput => dispatch({type: 'focusChange', payload: focusedInput})}
startDate={state.startDate} // Date or null
endDate={state.endDate} // Date or null
focusedInput={state.focusedInput} // START_DATE, END_DATE or null
/>
)
}