How to Make a Calendar App with React
Tags: react, javascript, tutorial, webdev
Published: 2020-01-20 00:49:01

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

For many applications, recording dates is an important feature. Having a calendar is often a handy feature to have. Fortunately, many developers have made calendar components that other developers can easily add to their apps.

React has many calendar widgets that we can add to our apps. One of them is React Big Calendar. It has a lot of features. It has a month, week, and day calendar. Also, you can navigate easily to today or any other days with back and next buttons. You can also drag over a date range in the calendar to select the date range. With that, you can do any manipulation you want with the dates.

In this article, we will make a simple calendar app where users can drag over a date range and add a calendar entry. Users can also click on an existing calendar entry and edit the entry. Existing entries can also be deleted. The form for adding and editing the calendar entry will have date and time pickers to select the date and time.

We will save the data on the back end in a JSON file.

We will use React to build our app. To start, we run:

```bash
npx create-react-app calendar-app
```

to create the project.

Next we have to install a few packages. We will use Axios for HTTP requests to our back end, Bootstrap for styling, MobX for simple state management, React Big Calendar for our calendar component, React Datepicker for the date and time picker in our form , and React Router for routing.

To install them, we run:

```bash
npm i axios bootstrap mobx mobx-react moment react-big-calendar react-bootstrap react-datepicker react-router-dom
```

With all the packages installed, we can start writing the code. First, we replace the existing code in `App.js` with:

```jsx
import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
import "react-big-calendar/lib/css/react-big-calendar.css";
import "react-datepicker/dist/react-datepicker.css";
const history = createHistory();
function App({ calendarStore }) {
  return (
    <div>
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Calendar App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} calendarStore={calendarStore} />
          )}
        />
      </Router>
    </div>
  );
}
export default App;
```

We add the React Bootstrap top bar in here with a link to the home page. Also, we add the route for the home page in here with the MobX `calendarStore` passed in.

Also, we import the styles for the date picker and calendar here so that we can use them throughout the app.

Next in `App.css` , replace the existing code with:

```css
.page {
  padding: 20px;
}
.form-control.react-datepicker-ignore-onclickoutside,
.react-datepicker-wrapper {
  width: 465px !important;
}
.react-datepicker__current-month,
.react-datepicker-time__header,
.react-datepicker-year-header,
.react-datepicker__day-name,
.react-datepicker__day,
[class^="react-datepicker__day--*"],
.react-datepicker__time-list-item {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
    "Droid Sans", "Helvetica Neue", sans-serif;
}
```

to add some padding to our page, change the width of the datepicker input and change the font of the datepicker.

Next create a file called `CalendarForm.js` in the `src` folder and add:

```jsx
import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import DatePicker from "react-datepicker";
import Button from "react-bootstrap/Button";
import {
  addCalendar,
  editCalendar,
  getCalendar,
  deleteCalendar
} from "./requests";
import { observer } from "mobx-react";
const buttonStyle = { marginRight: 10 };
function CalendarForm({ calendarStore, calendarEvent, onCancel, edit }) {
  const [start, setStart] = React.useState(null);
  const [end, setEnd] = React.useState(null);
  const [title, setTitle] = React.useState("");
  const [id, setId] = React.useState(null);
React.useEffect(() => {
    setTitle(calendarEvent.title);
    setStart(calendarEvent.start);
    setEnd(calendarEvent.end);
    setId(calendarEvent.id);
  }, [
    calendarEvent.title,
    calendarEvent.start,
    calendarEvent.end,
    calendarEvent.id
  ]);
const handleSubmit = async ev => {
    ev.preventDefault();
    if (!title || !start || !end) {
      return;
    }
if (+start > +end) {
      alert("Start date must be earlier than end date");
      return;
    }
    const data = { id, title, start, end };
    if (!edit) {
      await addCalendar(data);
    } else {
      await editCalendar(data);
    }
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };
  const handleStartChange = date => setStart(date);
  const handleEndChange = date => setEnd(date);
  const handleTitleChange = ev => setTitle(ev.target.value);
const deleteCalendarEvent = async () => {
    await deleteCalendar(calendarEvent.id);
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };
return (
    <Form noValidate onSubmit={handleSubmit}>
      <Form.Row>
        <Form.Group as={Col} md="12" controlId="title">
          <Form.Label>Title</Form.Label>
          <Form.Control
            type="text"
            name="title"
            placeholder="Title"
            value={title || ""}
            onChange={handleTitleChange}
            isInvalid={!title}
          />
          <Form.Control.Feedback type="invalid">{!title}</Form.Control.Feedback>
        </Form.Group>
      </Form.Row>
<Form.Row>
        <Form.Group as={Col} md="12" controlId="start">
          <Form.Label>Start</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={start}
            onChange={handleStartChange}
          />
        </Form.Group>
      </Form.Row>
<Form.Row>
        <Form.Group as={Col} md="12" controlId="end">
          <Form.Label>End</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={end}
            onChange={handleEndChange}
          />
        </Form.Group>
      </Form.Row>
      <Button type="submit" style={buttonStyle}>
        Save
      </Button>
      <Button type="button" style={buttonStyle} onClick={deleteCalendarEvent}>
        Delete
      </Button>
      <Button type="button" onClick={onCancel}>
        Cancel
      </Button>
    </Form>
  );
}
export default observer(CalendarForm);
```

This is the form for adding and editing the calendar entries. We add the React Bootstrap form in here by adding the `Form` component. The `Form.Control` is also from the same library. We use it for the `title` text input.

The other 2 fields are the start and end dates. We use React Datepicker in here to let use select the start and end dates of a calendar entry. In addition, we enable the time picker to let users pick the time.

There are change handlers in each field to update the values in the state so users can see what they entered and let them submit the data later. The change handlers are `handleStartChange` , `handleEndChange` and `handleTitleChange` . We set the states with the setter functions generated by the `useState` hooks.

We use the `useEffect` callback to set the fields in the `calendarEvent` prop to the states. We pass all the fields we want to set to the array in the second argument of the `useEffect` function so that the states will be updated whenever the latest value of the `calendarEvent` prop is passed in.

In the `handleSubmit` function, which is called when the form Save button is clicked. we have to call `ev.preventDefault` so that we can use Ajax to submit our form data.

If data validation passes, then we submit the data and get the latest and store them in our `calendarStore` MobX store.

We wrap `observer` outside the `CalendarForm` component so that we always get the latest values from `calendarStore` .

Next we create our home page. Create a `HomePage.js` file in the `src` folder and add:

```jsx
import React from "react";
import { Calendar, momentLocalizer } from "react-big-calendar";
import moment from "moment";
import Modal from "react-bootstrap/Modal";
import CalendarForm from "./CalendarForm";
import { observer } from "mobx-react";
import { getCalendar } from "./requests";
const localizer = momentLocalizer(moment);
function HomePage({ calendarStore }) {
  const [showAddModal, setShowAddModal] = React.useState(false);
  const [showEditModal, setShowEditModal] = React.useState(false);
  const [calendarEvent, setCalendarEvent] = React.useState({});
  const [initialized, setInitialized] = React.useState(false);
  const hideModals = () => {
    setShowAddModal(false);
    setShowEditModal(false);
  };
  const getCalendarEvents = async () => {
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    setInitialized(true);
  };
  const handleSelect = (event, e) => {
    const { start, end } = event;
    const data = { title: "", start, end, allDay: false };
    setShowAddModal(true);
    setShowEditModal(false);
    setCalendarEvent(data);
  };
  const handleSelectEvent = (event, e) => {
    setShowAddModal(false);
    setShowEditModal(true);
    let { id, title, start, end, allDay } = event;
    start = new Date(start);
    end = new Date(end);
    const data = { id, title, start, end, allDay };
    setCalendarEvent(data);
  };
  React.useEffect(() => {
    if (!initialized) {
      getCalendarEvents();
    }
  });
  return (
    <div className="page">
      <Modal show={showAddModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Add Calendar Event</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={false}
          />
        </Modal.Body>
      </Modal>
      <Modal show={showEditModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Calendar Event</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={true}
          />
        </Modal.Body>
      </Modal>
      <Calendar
        localizer={localizer}
        events={calendarStore.calendarEvents}
        startAccessor="start"
        endAccessor="end"
        selectable={true}
        style={{ height: "70vh" }}
        onSelectSlot={handleSelect}
        onSelectEvent={handleSelectEvent}
      />
    </div>
  );
}
export default observer(HomePage);
```

We get the calendar entries and populate them in the calendar here. The entries are retrieved from back end and then saved into the store. In the `useEffect` callback, we set get the items when the page loads. We only do it when `initialized` is false so we won’t be reloading the data every time the page renders.

To open the modal for adding calendar entries, we set the `onSelectSlot` prop with our handler so that we can call `setShowAddModal` and `setCalendarEvent` to set open the modal and set the dates before opening the add calendar event modal.

Similarly, we set the `onSelectEvent` modal with the `handleSelectEvent` handler function so that we open the edit modal and set the calendar event data of the existing entry.

Each Modal have the `CalendarForm` component inside. We pass in the function for closing the modals into the form so that we can close them from the form. Also, we pass in the `calendarStore` and `calendarEvent` so that they can be manipulated in the `CalendarForm`.

We wrap `observer` outside the `CalendarForm` component so that we always get the latest values from `calendarStore` .

Next in `index.js` , we replace the existing code with:

```jsx
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { CalendarStore } from "./store";
const calendarStore = new CalendarStore();
ReactDOM.render(
  <App calendarStore={calendarStore} />,
  document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
```

so that we can pass in the MobX `calendarStore` into the root `App` component.

Next create a `requests.js` file in the `src` folder and add:

```javascript
const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getCalendar = () => axios.get(`${APIURL}/calendar`);
export const addCalendar = data => axios.post(`${APIURL}/calendar`, data);
export const editCalendar = data =>
  axios.put(`${APIURL}/calendar/${data.id}`, data);
export const deleteCalendar = id => axios.delete(`${APIURL}/calendar/${id}`);
```

These are the functions for making the HTTP calls to manipulate the calendar entries.

Next create`store.js` in the `src` folder and add:

```javascript
import { observable, action, decorate } from "mobx";
class CalendarStore {
  calendarEvents = [];
setCalendarEvents(calendarEvents) {
    this.calendarEvents = calendarEvents;
  }
}
CalendarStore = decorate(CalendarStore, {
  calendarEvents: observable,
  setCalendarEvents: action
});
export { CalendarStore };
```

to save the items in the store for access by all of our components.

Next in `index.html` , replace the existing code with:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Calendar App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>
```

to add the Bootstrap CSS and rename the title.

Now all the hard work is done. All we have to do is use JSON Server NPM package located at [https://github.com/typicode/json-server](https://github.com/typicode/json-server) for our back end.

Install it by running:

```bash
npm i -g json-server
```

Then run it by running:

```bash
json-server --watch db.json
```

In `db.json` , replace the existing content with:

```json
{  
  "calendar": []  
}
```

Next we run our app by running `npm start` in our app’s project folder and when the program asks you to run in a different port, select yes.