DEV Community

John Au-Yeung
John Au-Yeung

Posted on

How to Make a Calendar App with React

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:

npx create-react-app calendar-app
Enter fullscreen mode Exit fullscreen mode

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:

npm i axios bootstrap mobx mobx-react moment react-big-calendar react-bootstrap react-datepicker react-router-dom
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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:

.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;
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
Enter fullscreen mode Exit fullscreen mode

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:

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);
Enter fullscreen mode Exit fullscreen mode

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:

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();
Enter fullscreen mode Exit fullscreen mode

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:

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}`);
Enter fullscreen mode Exit fullscreen mode

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

Next createstore.js in the src folder and add:

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

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

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

<!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>
Enter fullscreen mode Exit fullscreen mode

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 for our back end.

Install it by running:

npm i -g json-server
Enter fullscreen mode Exit fullscreen mode

Then run it by running:

json-server --watch db.json
Enter fullscreen mode Exit fullscreen mode

In db.json , replace the existing content with:

{  
  "calendar": []  
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (12)

Collapse
 
khushi24699 profile image
Khushi Kumar

Attempted import error: 'decorate' is not exported from 'mobx'.
getting this error

Collapse
 
khushi24699 profile image
Khushi Kumar • Edited

so i changed decorate to makeObservable , but now i am getting this error

Error: [MobX] Cannot decorate undefined property: 'calendarEvents'

Collapse
 
aumayeung profile image
John Au-Yeung

Did you follow this guide?

mobx.js.org/observable-state.html

Collapse
 
aumayeung profile image
John Au-Yeung

You tried enabling decorators?

mobx.js.org/enabling-decorators.html

Collapse
 
khushi24699 profile image
Khushi Kumar

Yes after that I switched to makeObserver

Thread Thread
 
aumayeung profile image
John Au-Yeung

If you use decorators, then you don't use makeObserver.

The repo is at bitbucket.org/hauyeung/react-calen...

Thread Thread
 
khushi24699 profile image
Khushi Kumar • Edited

yes yes , it seems they stopped with decorator i guess
anyways thanks for the repository it works now :)

Thread Thread
 
aumayeung profile image
John Au-Yeung

No problem

Collapse
 
damon_developes profile image
Damon Schulz

I am attempting to use this calender app within a ToDoList application I am building. How can I pass the logged in user of my app their own Calendar? With the info they have saved in their database

Collapse
 
khushi24699 profile image
Khushi Kumar

hey i am kinda working on the same thing , if it is fine by you to share the repo would be a great help .

Collapse
 
aumayeung profile image
John Au-Yeung

Thanks for reading.

Sure. Let me find it and link it here.

Collapse
 
aumayeung profile image
John Au-Yeung

You can them from the API or store the user data on client side.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.