Horizontal Calendar - a simple date picker for React Native
Published: 2023-04-01 22:52:16

## Trailer
I decided to write this to help someone who may need to build a simple horizontal calendar picker for your mobile app. Today we are going to do so without having to use the cumbersome third party packages.

## Why?
Well, suppose you're building an app that requires you(or the user) to select a date and other events that would be dependent on the date selected e.g. _movie booking, event planner, travel/event planner, flight/bus booking or even an appointment manager(which I'll be launching in a few weeks)_ 

Okay, that's enough foreplay; lets dive into the cool stuff. I'll add a repo at the end.

____

## Let the fun begin

### Create a new project.
Initialize a new [expo](https://expo.dev/) app - We are using Expo because its 2023 and its freaking cool.

```
npx create-expo-app HzCalendar
cd HzCalendar
```

Now you can run any of the following commands depending on which device/simulator you're testing with. As an added bonus, you can run it on web too(expo is that good)

```
npm run android
npm run ios
npm run web
```


![First](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zgyqx4dopocodv4ceult.png)

____

### Install dependencies
We're only going to need only [moment](https://momentjs.com/) for working with dates. You may choose to use others like [date-fns](https://date-fns.org/) if you wish. 

```
npm install --save moment
```

#### 1. Calendar Component
We are going to have a calendar component that takes two props, an _onSelectDate_ and _selected_. The `selected` prop is so we change styles later. Lets create a `components` folder, then create a `Calendar` component.

- we are going to track 3 states: an empty `date` to start with, a `scrollPosition` and a `currentMonth`.
- later we will use the `scrollPosition` to track the current month.

First we are gonna use a `for loop` to generate the next 10 days. We will then parse the days with moment and update the dates state.

> At this point I assume you understand the basic javascript functions, and how react works when passing props and state data. I'll also add a few styles, so I hope you understand how styling works in React Native.

Then we will display the dates in a horizontal scrollview.

### 2. Date Component
We need to abstract the date component. Lets create a `Date` component in the `Components` folder
- This will be a simple `TouchableOpacity` button that will call `onSelectDate`
- Our `Date` component will take 3 props, a `date` that will be displayed, the `onSelectDate` function we passed to calendar, and the `selected` prop too.
- We'll use the `selected` prop to give the selected date different styling.

```js
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native'
import moment from 'moment'

const Date = ({ date, onSelectDate, selected }) => {
  /**
   * use moment to compare the date to today
   * if today, show 'Today'
   * if not today, show day of the week e.g 'Mon', 'Tue', 'Wed'
   */
  const day = moment(date).format('YYYY-MM-DD') === moment().format('YYYY-MM-DD') ? 'Today' : moment(date).format('ddd')
  // get the day number e.g 1, 2, 3, 4, 5, 6, 7
  const dayNumber = moment(date).format('D')

  // get the full date e.g 2021-01-01 - we'll use this to compare the date to the selected date
  const fullDate = moment(date).format('YYYY-MM-DD')
  return (
    <TouchableOpacity
      onPress={() => onSelectDate(fullDate)}
      style={[styles.card, selected === fullDate && { backgroundColor: "#6146c6" }]}
    >
      <Text
        style={[styles.big, selected === fullDate && { color: "#fff" }]}
      >
        {day}
      </Text>
      <View style={{ height: 10 }} />
      <Text
        style={[
          styles.medium,
          selected === fullDate && { color: "#fff", fontWeight: 'bold', fontSize: 24 },
        ]}
      >
        {dayNumber}
      </Text>
    </TouchableOpacity>
  )
}

export default Date

const styles = StyleSheet.create({
  card: {
    backgroundColor: '#eee',
    borderRadius: 10,
    borderColor: '#ddd',
    padding: 10,
    marginVertical: 10,
    alignItems: 'center',
    height: 90,
    width: 80,
    marginHorizontal: 5,
  },
  big: {
    fontWeight: 'bold',
    fontSize: 20,
  },
  medium: {
    fontSize: 16,
  },
})
```

Then we call the `Date` component in the `Calendar` component and pass each date in the scrollview.

```js
import { useState, useEffect } from 'react'
import { StyleSheet, Text, View, ScrollView } from 'react-native'
import moment from 'moment'
import Date from './Date'

const Calendar = ({ onSelectDate, selected }) => {
  const [dates, setDates] = useState([])
  const [scrollPosition, setScrollPosition] = useState(0)
  const [currentMonth, setCurrentMonth] = useState()

  // get the dates from today to 10 days from now, format them as strings and store them in state
  const getDates = () => {
    const _dates = []
    for (let i = 0; i < 10; i++) {
      const date = moment().add(i, 'days')
      _dates.push(date)
    }
    setDates(_dates)
  }

  useEffect(() => {
    getDates()
  }, [])

  return (
    <>
      <View style={styles.centered}>
        <Text style={styles.title}>Current month</Text>
      </View>
      <View style={styles.dateSection}>
        <View style={styles.scroll}>
          <ScrollView
            horizontal
            showsHorizontalScrollIndicator={false}
          >
            {dates.map((date, index) => (
              <Date
                key={index}
                date={date}
                onSelectDate={onSelectDate}
                selected={selected}
              />
            ))}
          </ScrollView>
        </View>
      </View>
    </>
  )
}

export default Calendar

const styles = StyleSheet.create({
  centered: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  dateSection: {
    width: '100%',
    padding: 20,
  },
  scroll: {
    height: 150,
  },
})
```

Now we update our `App.js` file to display the `Calendar` component.
- We'll add a `selectedDate` state that we shall pass to `Calendar` props.

```js
import { StatusBar } from 'expo-status-bar';
import { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import Calendar from './components/Calendar';

export default function App() {
  const [selectedDate, setSelectedDate] = useState(null);

  return (
    <View style={styles.container}>
      <Calendar onSelectDate={setSelectedDate} selected={selectedDate} />
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
```

Now we have a working Horizontal scroll calendar component that looks pretty much like this.(if you select a date, you'll notice the styling changes - that's that Stylesheet magic we added in the `Date` component)


![almost there](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ihwvgbe0ca33ax5xtny.png)

____

### Bonus - Current month
Now let's use the `scrollPosition` to determine the month to be displayed above our dates.
- We'll update our `Calendar` component and track the scroll position on the horizontal scrollview and update the `scrollPosition` state.
- Then we'll use the `scrollPosition` and `moment` to generate the current month of the date in view.

> **PS**: this `onScroll` functionality may not be the best implementation. However I challenge you to come up with a better one. I know there's one. If you do, don't hesitate to open a PR on the [repo](https://github.com/kharioki/HzCalendar).

```js
import { useState, useEffect } from 'react'
import { StyleSheet, Text, View, ScrollView } from 'react-native'
import moment from 'moment'
import Date from './Date'

const Calendar = ({ onSelectDate, selected }) => {
  const [dates, setDates] = useState([])
  const [scrollPosition, setScrollPosition] = useState(0)
  const [currentMonth, setCurrentMonth] = useState()

  // ... same as before

  /**
   * scrollPosition is the number of pixels the user has scrolled
   * we divide it by 60 because each date is 80 pixels wide and we want to get the number of dates
   * we add the number of dates to today to get the current month
   * we format it as a string and set it as the currentMonth
   */
  const getCurrentMonth = () => {
    const month = moment(dates[0]).add(scrollPosition / 60, 'days').format('MMMM')
    setCurrentMonth(month)
  }

  useEffect(() => {
    getCurrentMonth()
  }, [scrollPosition])

  return (
    <>
      <View style={styles.centered}>
        <Text style={styles.title}>{currentMonth}</Text>
      </View>
      <View style={styles.dateSection}>
        <View style={styles.scroll}>
          <ScrollView
            horizontal
            showsHorizontalScrollIndicator={false}
            // onScroll is a native event that returns the number of pixels the user has scrolled
            scrollEventThrottle={16}
            onScroll={(e) => setScrollPosition(e.nativeEvent.contentOffset.x)}
          >
            {dates.map((date, index) => (
              <Date
                key={index}
                date={date}
                onSelectDate={onSelectDate}
                selected={selected}
              />
            ))}
          </ScrollView>
        </View>
      </View>
    </>
  )
}

export default Calendar

const styles = StyleSheet.create({
  centered: {
    justifyContent: 'center',
    alignItems: 'center',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  dateSection: {
    width: '100%',
    padding: 20,
  },
  scroll: {
    height: 150,
  },
})
```

Now we can see the month and when you scroll to a new month, it should change the month displayed above.


![finally](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mrlym1uw5lsofodfhcgk.png)

____

### Fun exercise...
_Try using the `Animated` API to center selected date or even make it feel like a wheel_.

____

You can find all the code in this [repo](https://github.com/kharioki/HzCalendar). PRs are welcome.