EcmaScript 2015 (and later), React 16.8 (and up), and modern programming style, is the key to implementing powerful prototype applications with simple clean code.

This tutorial demonstrates simple ways to get going with React. It covers most of the features modern prototype apps need, including databases and authentication, using only functions, JSX, and a few libraries for CSS and Firebase. No JavaScript classes, lifecycle methods, state manager, or higher-order components.

If you have not looked at React before, see this short list of basic concepts.

Setting up

Install a React-savvy editor

An editor that knows about React code can save hours of time. I use Visual Studio Code -- note: not Visual Studio! It runs on MacOS, Windows, and Linux. That's handy for teams with different development machines.

An up to date list of good choices is here.

Install Node

Install NodeJS. This is the most tedious step, but doesn't require much thinking. If you have NodeJS installed, be sure it's the latest LTS (long-term support) version, i.e., at least version 10.

Use create-react-app

Start new applications with this command:

npx create-react-app new-app-name

The create-react-app tool makes starting a new React app a one-step process. It creates a new directory, with a default set of React files, including a .gitignore file that will avoid accidentally storing 1000s of Node libraries on github. It also installs npm scripts for running a local server, testing, and creating a deployable production build.

For this demonstration, we're making a class conflict detector.

npx create-react-app scheduler

Test to make sure everything installs and runs.

cd scheduler
npm install

The install downloads a ton of files. If you get warnings about a library that requires a peer of typescript@* but none is installed, do this:

npm install typescript --save-dev

Note the last part is "dash dash save dash dev".

Now do

npm run start

This starts a local web server and launches your browser on a home page create-react-app created. After 10 to 20 seconds, a web page should appear with the animated React logo. You can leave this page open and the server running. As you edit and save files, React will rebuild and reload the local web site. If you make a mistake, you'll see error messages in the terminal console, the browser, and/or the browser's developer console.

[Optional] Set up a remote repo and site

Create a remote repo for your app on Github, Bitbucket, Gitlab, or wherever, do a local git init, and so on.

Set up a remote web site for testing, showing to users, etc. I like Firebase, but you can use AWS, Heroku, or whatever you are familiar with.

Know your files

create-react-app creates a short public/index.html file. There is almost no code in that file. You rarely need to edit it, except to change the page title.

Let's change the web page title that shows up on the browser tab and the browser history. Replace this line in public/index.html

<title>React App</title>

with this

<title>CS Course Scheduler</title>

This new title will show up as soon as we save.

The HTML file gets its content by loading src/index.js. This JavaScript file has almost no code as well. The most important line is this one near the end:

ReactDOM.render(<App />, document.getElementById('root'));

You rarely need to edit this file either, except to import additional JavaScript libraries, or modify what data is passed to App.

Slice 1: Our first React code

The file src/App.js is the heart of your application. Unless specified otherwise, all our editing below will be in that file.

We're going to write a simple React app for quickly finding classes that don't conflict in meeting time. We'll do this in tiny slices that make something new happen on-screen in every step.

We need some data. Just to get things started, we'll make a data object with a title, and write just enough code to print the title.

Replace the entire contents of the src/App.js file that create-react-app made with the following text.

import React from 'react';

const schedule = {
  title: "CS Courses for 2018-2019"
};

const App = () =>  (
  <div>
    <h1>{ schedule.title }</h1>
  </div>
);

export default App;

If the local server is still running, saving this file should change the page to show the title "CS Courses for 2018-2019".

The first line of the code makes React available to our JavaScript. The last line makes the function App available to any script that imports this file, such as this line at the top of src/index.js.

import App from './App';

There is nothing special about the name App. It is just the name that create-react-app uses by default.

The rest of our code in src/App.js defines a variable schedule with our data, and the function App. The function just returns some HTML that displays the title in the data.

App is defined using JavaScript's modern arrow function expresssion syntax.

More on arrow syntax.

The function uses React's JSX feature. JSX lets us write HTML directly, inserting JavaScript values as needed inside curly braces.

<div>
  <h1>{ schedule.title }</h1>
</div>

Scripts installed by create-react-app translate the JSX into regular JavaScript code to build the actual HTML.

More on JSX syntax.

The function App gets called by this line of code in index.js:

ReactDOM.render(<App />, document.getElementById('root'));

<component /> is HTML shorthand for <component></component>. All JSX components must be closed one way or the other.

We've finished our first slice. We've have a working skeleton of our app!

CodePen

To see the output, click on the Result button in the CodePen below. To play with the code, click on the icon in the upper right of the CodePen.

See the Pen CourseSchedule Title by Chris Riesbeck (@criesbeck) on CodePen.

Slice 2: Showing a list of courses

Of course, what we really want to display are the courses. That will need a loop. We could write it in App, but it would better to refactor App into two new components, passing each one just the data it needs:

  • A banner with the title
  • A list with the courses

Components are things we can invent as we need them, to organize our React code.

Let's add some courses to our data object, and add components for the banner and course list to our App function:

import React from 'react';

const schedule = {
  "title": "CS Courses for 2018-2019",
  "courses": [
    {
      "id": "F101",
      "title": "Computer Science: Concepts, Philosophy, and Connections",
      "meets": "MWF 11:00-11:50"
    },
    {
      "id": "F110",
      "title": "Intro Programming for non-majors",
      "meets": "MWF 10:00-10:50"
    },
    {
      "id": "F111",
      "title": "Fundamentals of Computer Programming I",
      "meets": "MWF 13:00-13:50"
    },
    {
      "id": "F211",
      "title": "Fundamentals of Computer Programming II",
      "meets": "TuTh 12:30-13:50"
    }
  ]
};

const App = () =>  (
  <div>
    <Banner title={ schedule.title } />
    <CourseList courses={ schedule.courses } />
  </div>
);

export default App;

Our new App creates two component: Banner and CourseList. It passes the title to the banner and the list of courses to the course list, using HTML attributes.

Component names must be capitalized, so React can tell them apart from normal HTML such as div and span.

Component are just functions you define to return JSX. When React sees a component in JSX, it calls the function with the same name. Whatever the function returns replaces the component in the JSX that called it.

More on functional components.

React collects any attributes attached to a component into a props object, and passes that object to the function. The keys of the object are the attributes in the component call, and the values are the attribute values. So

<Banner title={ schedule.title } />

means the function Banner will be passed the props object { "title": "CS Courses for 2018-2019" }.

Do not use the attribute children. props.children has a special purpose in React.

We could define Banner like this:

const Banner = props => (
  <h1>{props.title}</h1>
)

But modern JavaScript lets you use destructuring syntax to get the values you want from an object. It not only makes some functions a little shorter, it makes the parameter list explicit about what the function expects to receive.

const Banner = ({ title }) => (
  <h1>{ title }</h1>
);

If you forget the curly braces in the parameter list and write

const Banner = (title) =>  (
  <h1>{ title }</h1>
);
you will get the error "Objects are not valid as a React child" when you load the page.

The CourseList component will need to loop over the list of courses, producing HTML for each one. In modern JavaScript, looping is best done using mapping functions, such as map and filter.

More on mapping methods..

The course list component should just map over the courses and return a course component for each one. React lets us nest lists of components inside JSX, so our code can look like this:

const CourseList = ({ courses }) => (
  <div>
    { courses.map(course => <Course course={ course } />) }
  </div>
);

Our courses will be buttons so the user can select and de-select them. We have to do a little work to get user-friendly button text. Instead of just the title, we'd like to show the term and course number. We can get this from the ID. For example, "F101" is in the fall term, and its course number is 101. We define utility functions to get the term and number from the ID, leading to this code for Course:

const terms = { F: 'Fall', W: 'Winter', S: 'Spring'};

const getCourseTerm = course => (
  terms[course.id.charAt(0)]
);

const getCourseNumber = course => (
  course.id.slice(1, 4)
)
  
const Course = ({ course }) => (
  <button>
    { getCourseTerm(course) } CS { getCourseNumber(course) }: { course.title }
  </button>
);

The above will work fine. It produces the heading and a list of the sample courses. But you will see a warning from React on the console about elements in a list without keys. The reason is a bit technical but the fix is to add a key attribute with a unique, unchanging ID. That's easy in this case.

const CourseList = ({ courses }) => (
  <div>
    { courses.map(course => <Course key={course.id} course={ course } />) }
  </div>
);

More on list keys.

We've finished our second slice! We have a working app that takes data about a list of courses and displays them on a web page.

CodePen

See the Pen TeamList by Chris Riesbeck (@criesbeck) on CodePen.

Slice 3: Styling our app

There are two ways to style React apps.

  • Use CSS classes from a CSS library like Bootstrap, Bulma, or MaterialUI
  • Use a library that defines styled React components

The second approach is usually faster and simpler. Here we'll use rbx. It's not very well known, but it's very simple to use, with clean documentation and clean code.

More popular but more complex React style libraries include Material-UI, Semantic UI React (based on but different from Semantic UI), React BootStrap, and ReactStrap. For a measure of usage, see this chart showing the number of downloads from Github for each package.

To install rbx in your project:

npm install rbx

Then, in any React file that needs styling, import the core CSS file, and the specific styled components that you need. E.g., if you need the container, title, and button components for your code:

import 'rbx/index.css';
import { Button, Container, Title } from 'rbx';

More on importing CSS and images.

Then use the appropriate rbx components. The documentation lists everything with example HTML and output.

One of the advantages of styled components for prototyping is that a React-savvy editor will quickly tell you if you fail to import a component your code needs, or are importing a component that's not being used.

A simple place to start is our banner. It's just a title, so the styled version is pretty obvious:

const Banner = ({ title }) => (
  <Title>{ title }</Title>
);

Our courses are buttons and there's a nice rbx button component.

const Course = ({ course }) => (
  <Button>
    { getCourseTerm(course) } CS { getCourseNumber(course) }: { course.title }
  </Button>
);

Our course list is a group of buttons. Groups of buttons are pretty common so most libraries have a component for that. Some require you to import a separate ButtonGroup component, but rbx uses Button.Group. You get it automatically when you import Button with rbx.

const CourseList = ({ courses }) => (
  <Button.Group>
    {courses.map(course => <Course key={course.id} course={ course } />)}
  </Button.Group>
);

Finally, our app just holds the banner and course list. rbx has a general container component for that purpose.

const App = () =>  (
  <Container>
    <Banner title={ schedule.title } />
    <CourseList courses={ schedule.courses } />
  </Container>
);

We've finished our third slice!

CodePen

Note: I couldn't find a way to load rbx into CodePen, so the CodePen version uses the Bulma CSS classes directly, to get the same output.

See the Pen TeamList Styled by Chris Riesbeck (@criesbeck) on CodePen.

Slice 4: Fetching data

Creating trackable state

Most applications get their data from a server. Let's do that. The data we're going to fetch is this JSON.

We're going to need to store that data locally in some trackable state variable, so that React can tell when it changes and the page should be updated. This is the "react" part of React. You don't need to write any code to update your page. You just update your data. React does the rest, efficiently and robustly.

Global variables lead to hard to maintain code, so React makes it possible to store state in components. If just one part of your code needs to have a list of selected items, that list can be stored in trackable state in the component that needs that list.

In React 16.8 and up, you create trackable state in a component with useState(). To use useState, you need to import it at the top of your App.js file.

import React, { useState } from 'react';

useState() is called a Hook, because it hooks into the internals of the React system. How it works is complicated, but using it is not.

You call useState(value) inside a component to create a local state variable for that component. useState(value) takes an initial value, and returns a two-element array with the initial value and a function. The function will be a setter to use to update the state. Here's an example.

const Scoreboard = () => {
  const [score, setScore] = useState(0);
  ...

This uses array destructuring to initialize score to 0 and set setScore to the update function. Any changes to score should be done by calling setScore(new-value). When that happens, React will re-render any components that depend on score.

The initial value should be something that works with your component code. Zero or null or an empty string is fine if your component JSX can handle that.

In our application, our components want a course schedule object, so our initial value should be an object with an empty title and empty list of courses.

const App = () => {
  const [schedule, setSchedule] = useState({ title: '', courses: [] });
  ...
};

The body of the App arrow function has become a statement block, with curly braces, because we need more than a single expression.

To show the user that there is no data yet, we modify our banner component slightly, to show "[loading...]" until such time as data has been received.

const Banner = ({ title }) => (
  <Title>{ title || '[loading...]' }</Title>
);

More on useState().

Getting data for components

Now that we have a schedule state variable, we need to fetch some data to put into it. I have put our data at the URL

'https://courses.cs.northwestern.edu/394/data/cs-courses.php'

This URL can be fetched from any server, including your local test server, without getting a CORS exception.

If you want to serve the file locally, download the JSON, put it in the file public/data/cs-courses.json in your app directory. Then you can fetch it with the URL '/data/cs-courses.json'.

Fetching data is typically done by calling an asynchronous function. Asynchronous functions internally are passed three functions. An asynchronous function will call the first function and return immedately, so that the application can go back to responding to other events. The second function, called the success callback, will be called with the value returned by the first function, unless there is an error, in which case the third function, called the failure callback is called.

Modern JavaScript has a special await keyword that can be used to write asynchronous code more readably, without writing explicit function wrappers. await can be inside any function marked with the async keyword. Such a function itself becomes an asynchronous function.

The standard function to get data over the web is fetch(). This is an asynchronous function because it might take many seconds to get the response. When the response is received, it can be checked to see if it has data or a server message. If it has data, e.g., JSON, it can be parsed with another asynchronous function, response.json().

Here's how those two asynchronous functions could be called using async and await, to get our schedule JSON and store it with the setSchedule() function created by useState():

const fetchSchedule = async () => {
  const response = await fetch(url);
  if (!response.ok) throw response;
  const json = await response.json();
  setSchedule(json);
};

When a browser executing an async function gets to an await call, the browser does the call but puts the rest of the function code into a implicit success callback. When the asynchronous call returns a value, the browser calls the success callback.

More about asynchronous code.

The trick is to integrate this asynchronous event processing into the React rendering cycle. We want to fetch data only when a component that needs it is added to a page, but not every time it is re-rendered.

In React, you call useEffect(function) inside a component to tell React to call function whenever that component is added to the page or updated. useEffect() is another Hook function.

To use useEffect, import it at the top of your App.js file.

import React, { useState, useEffect } from 'react';

We can define and call fetchSchedule() inside useEffect() like this:

useEffect(() => {
  const fetchSchedule = async () => {
    const response = await fetch(url);
    if (!response.ok) throw response;
    const json = await response.json();
    setSchedule(json);
  }
  fetchSchedule();
})

This works but has a serious flaw. By default, the function passed to useEffect() is called whenever the component is added or updated. React apps can update the page very frequently, sometimes on every keystroke. We don't want to do a fetch everytime the component is updated. That could get our app kicked off a network service for violating service limits!

You can tell useEffect() to run the function only on updates where specific state variables have changed. You pass an array of those variables as the second argument. If the list is empty, then the useEffect() function will only be called when the component is added. Omitting the second argument tells React to run the function on all updates.

useEffect(() => {
  const fetchSchedule = async () => {
    const response = await fetch(url);
    if (!response.ok) throw response;
    const json = await response.json();
    setSchedule(json);
  }
  fetchSchedule();
}, [])

Putting this all together leads to this code to load the course schedule into our App.

const App = () => {
  const [schedule, setSchedule] = useState({ title: '', courses: [] });
  const url = 'https://courses.cs.northwestern.edu/394/data/cs-courses.php';

  useEffect(() => {
    const fetchSchedule = async () => {
      const response = await fetch(url);
      if (!response.ok) throw response;
      const json = await response.json();
      setSchedule(json);
    }
    fetchSchedule();
  }, [])

  return (
    <Container>
      <Banner title={ schedule.title } />
      <CourseList courses={ schedule.courses } />
    </Container>
  );
};
The code for fetching data from the Firebase Realtime Database is slightly different. See the section on using Firebase with Hooks.

More on useEffect() with databases.

That's it. We can delete the code that created the sample schedule. We've finished our fourth slice!

CodePen

See the Pen CourseSchedule Fetch by Chris Riesbeck (@criesbeck) on CodePen.

Slice 5: Interactive filtering

Time to start implementing our course scheduling tool.

Filtering by term

First, let's implement filtering by term. We will want some buttons to select what term we're picking courses for. Only one term can be selected at a time. Only courses for the selected term should be visible. Initially the fall term is selected.

First, let's add a component to the interface for selecting the term. We could add it to the App or to the CourseList component. Since the app includes the title and maybe many other things, the course list seems like a better place.

const CourseList = ({ courses }) => (
  <React.Fragment>
    <TermSelector />
    <Button.Group>
      { termCourses.map(course => <Course key={ course.id } course={ course } />) }
    </Button.Group>
  </React.Fragment>
);

JSX syntax only allows one component to be returned. React.Fragment is a way to group several components into one without generating an unnecessary HTML element, such as a div.

Our term selector can just be a group of buttons, one for each term. We can get the term names using JavaScript's Object.values() function on our global terms data object.

Object.values(terms) // returns ["Fall", "Winter", "Spring"]

The term selector is a group of buttons. It looks better to have the buttons connected. With rbx this is done by adding the hasAddons prop.

const TermSelector = () => (
  <Button.Group hasAddons>
    { Object.values(terms)
        .map(value => <Button key={value}>{ value }</Button>
        )
    }
  </Button.Group>
);

Now we need a trackable state variable for the current term. It should be initialized to the fall term. We're going to need the term state in both the selector and the list of courses, so we'll create the state in CourseList:

const CourseList = ({ courses }) => {
  const [term, setTerm] = useState('Fall');
  return (
    <React.Fragment>
      <TermSelector />
      <Button.Group>
        { courses.map(course => <Course key={ course.id } course={ course } />) }
      </Button.Group>
    </React.Fragment>
  );
};

We can filter the courses by term like this:

const termCourses = courses.filter(course => term === getCourseTerm(course));

We use this list to make the course buttons.

const CourseList = ({ courses }) => {
  const [term, setTerm] = useState('Fall');
  const termCourses = courses.filter(course => term === getCourseTerm(course));
  
  return (
    <React.Fragment>
      <TermSelector />
      <Button.Group>
        { termCourses.map(course =>
           <Course key={ course.id } course={ course } />) }
      </Button.Group>
    </React.Fragment>
  );
};

If you run this, you should see only fall courses. If you change the initial value to 'Winter' or 'Spring', you should see only those courses. Make sure that's true.

Now we'd like to highlight the current term. That means we need to pass the term as a property to the term selector.

const CourseList = ({ courses }) => {
  const [term, setTerm] = useState('Fall');
  const termCourses = courses.filter(course => term === getCourseTerm(course));
  return (
    <React.Fragment>
      <TermSelector term={ term } />
      <Button.Group>
        { termCourses.map(course => <Course key={ course.id } course={ course } />) }
      </Button.Group>
    </React.Fragment>
  );
};

Then we change the term selector to highlight the selected button. In rbx, the color prop can be used to highlight a button. The color success is a nice green, so we'll use that. Since we will want to do the same thing with the course buttons, we'll make a function to return success when passed a true value, and null otherwise. React ignores props with null values.

const buttonColor = selected => (
  selected ? 'success' : null
);

Now we can change the term selector to highlight just the selected term this way:

const TermSelector = ({ term }) => (
  <Button.Group hasAddons>
  { Object.values(terms)
      .map(value => 
        <Button key={value}
          color={ buttonColor(value === term) }
          >
          { value }
        </Button>
      )
  }
  </Button.Group>
);

With this code in place, the button that matches term and only that button should be highlighted.

Now let's implement clicking on a term button to select the term. To do that, we need to pass setTerm to the term selector. We could do that with a second attribute, but I prefer to keep related values together. So we'll make and pass an object with term and setTerm bundled together. We could package them in an object like this:

{ term: term, setTerm: setTerm }

Because code like this comes up all the time, in EcmaScript 2015 it can be abbreviated to

{ term, setTerm }

So we can pass a state object with both values to the TermSelector component this way:

<TermSelector state={ { term, setTerm } } />

The outer braces tell JSX to insert a value. The inner braces make the state object.

Putting this all together gives:

const CourseList = ({ courses }) => {
  const [term, setTerm] = useState('Fall');
  const termCourses = courses.filter(course => term === getCourseTerm(course));
  return (
    <React.Fragment>
      <TermSelector state={ { term, setTerm } } />
      <Button.Group>
        { termCourses.map(course
          => <Course key={ course.id } course={ course } state={ { term, setTerm } } />) }
      </Button.Group>
    </React.Fragment>
  );
};

We need to change the term selector to take a term state object, rather than a term, and add an onClick function to each button that sets the term with the appropriate value when that button is clicked.

const TermSelector = ({ state }) => (
  <Button.Group hasAddons>
  { Object.values(terms)
      .map(value => 
        <Button key={value}
          color={ buttonColor(value === state.term) }
          onClick={ () => state.setTerm(value) }
          >
          { value }
        </Button>
      )
  }
  </Button.Group>
);

More on event handlers.

We finished the term filtering part of our slice! Clicking a term button should now instantly make that button highlight and all course buttons not for that term disappear.

CodePen

See the Pen CourseSchedule Term Filtering by Chris Riesbeck (@criesbeck) on CodePen.

Filtering by course conflicts

The other thing we want to do is show course conflicts. We'll do that by disabling all course buttons that conflict with courses selected so far.

To do that, we need a trackable state variable for the list of currently selected courses. That's easy.

const [selected, setSelected] = useState([]);

setSelected() isn't the most convenient function to pass to our course buttons. Selecting the button for a course needs to add that course to the list. De-selecting it should remove the course from the list. This is called toggling. So we should pass a function that makes toggling easy.

We have to be careful here. We do NOT want to modify the array of selected items. State values should be immutable. I.e., they should never be directly or destructively modified. New states should be constructed instead. We can do that here by using concat() to create an array with a new element, and filter() to create an array without an element, using code like this:

selected.includes(x) ? selected.filter(y => y !== x) : [x].concat(selected)

More on immutable states.

For cleaner code, we're going to encapsulate the creation of the state and the toggling function inside a subfunction called useSelection.

const useSelection = () => {
  const [selected, setSelected] = useState([]);
  const toggle = (x) => {
    setSelected(selected.includes(x) ? selected.filter(y => y !== x) : [x].concat(selected))
  };
  return [ selected, toggle ];
};

useSelection behaves like useState(). It returns two values, a state value and a state setting function. The state setting function, when given an object, will set the selected state to a copy of selected with the object added, if the object was not selected. It will set the selected state to a copy of selected without the object, if the object was selected.

The name useSelection is not accidental. Hook functions should only be called inside components and custom hooks. A custom hook is any function whose name begins with use that calls hook functions. If this seems like an odd rule, it is. The reason for it has to do with the Rules of Hooks. Those rules are required by the way React tracks which states go with which components.

Now that we have useSelection(), we can add it to our CourseList component. Since the Course component will need both the list of selected items and the toggle function, we will pass a schedule state object with those two things, as we did with the term state.

const CourseList = ({ courses }) => {
  const [term, setTerm] = useState('Fall');
  const [selected, toggle] = useSelection();
  const termCourses = courses.filter(course => term === getCourseTerm(course));
 
  return (
    <React.Fragment>
      <TermSelector state={ { term, setTerm } } />
      <Button.Group>
        { termCourses.map(course =>
           <Course key={ course.id } course={ course }
             state={ { selected, toggle } } />) }
      </Button.Group>
    </React.Fragment>
  );
};

In the Course component, we can now highlight selected courses and select / unselect courses, using code similar to the TermSelector component.

const Course = ({ course, state }) => (
  <Button color={ buttonColor(state.selected.includes(course)) }
    onClick={ () => state.toggle(course) }
    >
    { getCourseTerm(course) } CS { getCourseNumber(course) }: { course.title }
  </Button>
);

Now we should see courses highlight and unhighlight as we select them.

Now for the tricky part. When we select a course, we want to disable the buttons for any courses that conflict with the course just selected.

To do that, we need a function that can tell us when a course conflicts with a set of courses. If we define a function hasConflict(course, selected), then the Course component just needs a conditional disabled attribute:

const Course = ({ course, state }) => (
  <Button color={ buttonColor(state.selected.includes(course)) }
    onClick={ () => state.toggle(course) }
    disabled={ hasConflict(course, state.selected) }
    >
    { getCourseTerm(course) } CS { getCourseNumber(course) }: { course.title }
  </Button>
);

The function hasConflict(course, selected) should return true if a course has a conflict with any selected course.

const hasConflict = (course, selected) => (
  selected.some(selection => courseConflict(course, selection))
);

The function courseConflict(course1, course2) should return true if the courses are different but in the same term and overlap on some day and time. To calculate that efficiently over and over, we need a better data structure than a string like "TuTh 10:00-11:20". We will parse meeting strings for each course into a more structured form. For example, we will parse "TuTh 10:00-11:20" into

{ days: "TuTh" hours: { start: 600, end: 680 } }

The days have been separated out, and the start and end times have been converted into minutes from midnight. The following function does this, using a regular expression for the hard part:

const meetsPat = /^ *((?:M|Tu|W|Th|F)+) +(\d\d?):(\d\d) *[ -] *(\d\d?):(\d\d) *$/;

const timeParts = meets => {
  const [match, days, hh1, mm1, hh2, mm2] = meetsPat.exec(meets) || [];
  return !match ? {} : {
    days,
    hours: {
      start: hh1 * 60 + mm1 * 1,
      end: hh2 * 60 + mm2 * 1
    }
  };
};

To avoid constantly re-parsing the meeting strings, we'll add the new fields to each course, using the spread operator.

const addCourseTimes = course => ({
  ...course,
  ...timeParts(course.meets)
});

const addScheduleTimes = schedule => ({
  title: schedule.title,
  courses: schedule.courses.map(addCourseTimes)
});

We call addSchedule() when loading data in our App component.

const App = () => {
  const [schedule, setSchedule] = useState({ title: '', courses: [] });
  ...
  useEffect(() => {
    const fetchSchedule =  async () => {
      ...
      setSchedule(addScheduleTimes(json));
    }
    fetchSchedule();
  }, [])
  ...
};

Now we can define our courseConflict() functions:

const daysOverlap = (days1, days2) => ( 
  days.some(day => days1.includes(day) && days2.includes(day))
);

const hoursOverlap = (hours1, hours2) => (
  Math.max(hours1.start, hours2.start) < Math.min(hours1.end, hours2.end)
);

const timeConflict = (course1, course2) => (
  daysOverlap(course1.days, course2.days) && hoursOverlap(course1.hours, course2.hours)
);

const courseConflict = (course1, course2) => (
  course1 !== course2
  && getCourseTerm(course1) === getCourseTerm(course2)
  && timeConflict(course1, course2)
);

We've finished our fifth slice! We have a tool to select a valid set of courses for a term. There was a fair bit of code at the end, but most of it was for calculating time conflicts, not React.

CodePen

See the Pen CourseSchedule Course Filtering by Chris Riesbeck (@criesbeck) on CodePen.

Slice 6: Reading and writing data with Firebase

The above code is fine for apps that just need to retrieve data from some source, like Yelp or IMDB. But apps that need to maintain their own data need to work with a database. For prototyping, cloud databases make sense because they are free to use for small amounts of data and network traffic, and easy to set up.

I like Firebase because it provides three services for free that are commonly needed: hosting, authentication, and a real-time database. A real-time database is one that automatically notifies your web app when data changes. If you have not used Firebase, read my Firebase notes before continuing with this section.

Northwestern u.northwestern.edu accounts are not configured for Firebase access. Use your personal Google account if you have one, or create one.

Create your backend Firebase project

Create a project at Firebase for the Course Scheduler. Add the Firebase real-time database to it. Import this data into the database.

See the official documentation for how to create a backend real-time database, and import a JSON object into it.

The data is the same before, except that each course is stored under its course ID, instead of in an array, following recommended Firebase data design practice.

Boilerplate: Init, install, and import Firebase

The next few steps are standard procedure that you do for every React app that uses the Firebase real-time database.

  • Install the Firebase CLI with npm install -g firebase-tools
  • Connect your app to the Firebase project you created, with npm init
  • Install the Firebase Node modules with npm install firebase

For more details, see my notes.

Once everything is installed, you're ready to start adding Firebase imports and calls to your code.

In App.js in our course scheduler code, import the Firebase libraries.

import firebase from 'firebase/app';
import 'firebase/database';

In this same file, add the firebase configuration object for your project, initialize the firebase object, and create a "reference" to the database. You get the configuration object from your Firebase project settings page.

const firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "....",
  messagingSenderId: "...",
  appId: "..."
};

firebase.initializeApp(firebaseConfig);
const db = firebase.database().ref();

Fetching data from Firebase

We're finally ready to start adding code to fetch course data from Firebase. Where to put code to fetch data from a dynamically database is one of the more confusing aspects of React. Putting the code in the wrong place can lead to:

  • Pages that force the user to go to a new page and come back, or restart the web app, in order to see the new data.
  • Apps that run very slowly because they are fetching data from the database every few seconds.

With hooks, you put code to fetch data inside a useEffect() function. For our app, we'll do this in our App component, just as we did before with fetch.

const App = () => {
  const [schedule, setSchedule] = useState({ title: '', courses: [] });

  useEffect(() => {
    ...fetch data here...
  }, []);

  return (
    ...
  );
};

In Firebase, we can fetch data with once() or on(). on() gives us the automatic update feature we want. You pass on() the function to call whenever data changes. The data comes in an object called a snapshot. The val() method of the snapshot returns the JSON object, if any. So, to store the new data in our schedule, we can do this:

db.on('value', snap => {
  if (snap.val()) setSchedule(addScheduleTimes(snap.val());
});

This code has two problems. One is that the JSON in the snapshot has courses in an object, not an array. We need to either redo our app code, or transform the JSON before storing it in the state. We'll do the latter for simplicity, by making calling Object.values() in addScheduleTimes():

const addScheduleTimes = schedule => ({
  title: schedule.title,
  courses: Object.values(schedule.courses).map(addCourseTimes)
});

The second problem with our call to on() is that it doesn't catch any errors that might happen. Early on, you'll make many errors using Firebase. You should always include an error handler, even if it's one that just pops up an alert:

db.on('value', snap => {
  if (snap.val()) setSchedule(addScheduleTimes(snap.val()));
}, error => alert(error));

Putting this in our App component gives us

const App = () => {
  const [schedule, setSchedule] = useState({ title: '', courses: [] });

  useEffect(() => {
    db.on('value', snap => {
      if (snap.val()) setSchedule(addScheduleTimes(snap.val()))    ;
    }, error => alert(error));
  }, []);

  return (
    ...
  );
};

This code is missing one more thing. on() causes the browser to periodically check the database for changes. It will keep doing that every few seconds until the user leaves the page. But on a single page web app, components come and go all the time. For example, you might not want a financial web app checking for stock price changes unless the user has the stock price component visible on screen.

In React, removing a component from the page is called unmounting. Unmounting a component should stop any listening processes it has started. In Firebase, you stop listening by calling the off() method. You pass it the exact same function you passed to on(). To do this, you put it in a variable first.

For example, if we had called

const handleData = snap => {
  if (snap.val()) setSchedule(addScheduleTimes(snap.val()));
};
db.on('value', handleData, error => alert(error));

then to stop listening, we call

db.off('value', handleData);

In React Hooks, to run code when a component unmounts, you return that code in a no-argument function as the return value of the useEffect() call that called on().

That leads to this definition of App.

const App = () => {
  const [schedule, setSchedule] = useState({ title: '', courses: [] });

  useEffect(() => {
    const handleData = snap => {
      if (snap.val()) setSchedule(addScheduleTimes(snap.val()));
    }
    db.on('value', handleData, error => alert(error));
    return () => { db.off('value', handleData); };
  }, []);

  return (
    <Container>
      <Banner title={ schedule.title } />
      <CourseList courses={ schedule.courses } />
    </Container>
);
};

This code does what the previous fetch code did, and more. Now, if the course data is changed by anyone, including ourselves, handleData() will be called, updating our local state, and re-rendering our web page, with no additional work on our part.

Notice that there was no need for async or await.

Storing data on Firebase

Now that we have a real database, we can do something we couldn't before -- store data.

Let's assume the scheduler is being used by a faculty member who has the authority to reschedule courses if there's an undesirable course conflict. To avoid adding more complexity to our interface, we're going to implement moving a course in a very simple, albeit error-prone way: double-clicking a course button will display the current meeting string for the user to change and save.

Adding the ability to recognize double-clicks is fairly simple:

const Course = ({ course, state }) => (
<Button color={ buttonColor(state.selected.includes(course)) }
    onClick={ () => state.toggle(course) }
    onDoubleClick={ () => moveCourse(course) }
    disabled={ hasConflict(course, state.selected) }
    >
    { getCourseTerm(course) } CS { getCourseNumber(course) }: { course.title }
  </Button>
);

moveCourse() should prompt the user to enter a new meeting time, e.g., "MWF 15:00-15:50". If an invalid time is entered, it should ask again. If the user enters nothing or cancels, the prompt should exit. Otherwise, the new data should be saved.

We'll use the timeParts() function we defined before to check validity. This will accept some silly meeting times, but will at least guarantee that any meeting time stored can be parsed into a meeting time object. If the parsing fails, the code asks again, using recursion.

const moveCourse = course => {
  const meets = prompt('Enter new meeting data, in this format:', course.meets);
  if (!meets) return;
  const {days} = timeParts(meets);
  if (days) saveCourse(course, meets); 
  else moveCourse(course);
};

Finally, we get to the part where data is saved. Firebase has the four ways to save data. Of those, update() makes the most sense here, because we just want to update one field of one course.

const saveCourse = (course, meets) => {
  db.child('courses').child(course.id).update({meets})
    .catch(error => alert(error));
};

The Firebase part was the easiest part of all!

An important point to realize is that saving data to Firebase will send the changed data back to all appropriate listeners. That means that saving the data will cause listeners to update the local state, so that everything is in synch, with no additional work on our part.

Slice 7: Authentication with Firebase

A common mistake that people do when developing a new app is to start with sign up/log. That's misplaced effort. Logging in is not an interesting thing to user test. Start with the part of your app that is new and different.

But now that we've made it possible for someone see course conflicts, and make changes, it makes sense to only let logged in users make changes. This is called authentication and authorization. We'll only do authentication here. With hooks, Firebase, and a helper UI library, it is not too much code.

Enable authentication on Firebase

The first thing you have to do is enable authentication for your Fireabase project on the Firebase web console. At the moment, Google's documentation doesn't have a simple page describing this but it's not hard:

  • Go to Firebase console
  • Go to your project page
  • Click Authentication on the left
  • Click Sign-in method along the top
  • Click the method you want to enable.
  • In the panel that opens up, click enable and enter any required information
  • Click Save

That last step is easy to miss on the Firebase web site!

The two simplest methods to enable are Google and email. Google takes care of all the details. We'll just do Google authentication here. Click on that method, click enable, and click Save.

I recommend adding react-firebaseui as well. It makes it easy to add those "sign in with ..." buttons to your app.

npm install --save react-firebaseui

Import the following in App.js:

import 'firebase/auth';
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth';

You must have the first import to use authentication at all. The second import is for react-firebaseui.

In App.js add the following configuration object to your code

const uiConfig = {
  signInFlow: 'popup',
  signInOptions: [
    firebase.auth.GoogleAuthProvider.PROVIDER_ID
  ],
  callbacks: {
    signInSuccessWithAuthResult: () => false
  }
};

This tells the authentication component to display a "sign in with Google button". If we include more sign in options, buttons for those will appear as well.

For more on setting up Firebase authentication providers, see this video. Setting up Facebook authentication is particularly complex. The code is in this repo but it's old-style React. Follow the code patterns below for managing user state.

Decide what authentication means for your app

The above was boilerplate. Now we need to think about what we want to happen when users are and are not logged in. The most annoying pages are those that don't let you do anything unless you are logged in.

Clearly, we'll need a way for people to log in and log out. We want anyone to be able to select courses and see what conflicts arise. But we want only someone who is logged in to be able to change a course's meeting time.

Add a state for a signed in user

We need a state to hold the signed in user, or null.

const [user, setUser] = useState(null);

We're going to need to call setUser whenever the current user changes. We can tell the Firebase authentication library what function to call when authentication changes like this:

firebase.auth().onAuthStateChanged(listener);

The listener function will be passed an object with the current user. null is passed if there is no current user. setUser works just fine as a listener.

As with the Firebase on() listener, we install the listener in a useEffect() call.

useEffect(() => {
  firebase.auth().onAuthStateChanged(setUser);
}, []);

This should be a new useEffect() call in our component. We can do as many useEffect() calls in a component as we need.

We pass the user to our banner and course list components. Our new App component looks like this:

const App = () => {
  const [schedule, setSchedule] = useState({ title: '', courses: [] });
  const [user, setUser] = useState(null);

  useEffect(() => {
    const handleData = snap => {
      if (snap.val()) setSchedule(addScheduleTimes(snap.val()));
    };
    db.on('value', handleData, error => alert(error));
    return () => { db.off('value', handleData); };
  }, []);

  useEffect(() => {
    firebase.auth().onAuthStateChanged(setUser);
  }, []);

  return (
    <Container>
      <Banner title={ schedule.title } user={ user } />
      <CourseList courses={ schedule.courses } user={ user } />
    </Container>
  );
};

Add code to sign in and out

We'll put the login/signup logic in our banner, since that's at the top of the page. The banner should show the login option if the user is null, and the welcome message plus a logout button if the user is not null.

As always, as soon as components get complicated, it pays to refactor into subcomponents. We'll define one component for welcoming logged-in users and another for the log in option.

const Banner = ({ user, title }) => (
  <React.Fragment>
    { user ? <Welcome user={ user } /> : <SignIn /> }
    <Title>{ title || '[loading...]' }</Title>
  </React.Fragment>
);

Our welcome component will display the user's name and a logout button. In Firebase authentication, you log the current user out with

firebase.auth().signOut()

I experimented with rbx for a bit. The Message component looked like what I wanted, so my welcome component ended up as:

const Welcome = ({ user }) => (
  <Message color="info">
    <Message.Header>
      Welcome, {user.displayName}
      <Button primary onClick={() => firebase.auth().signOut()}>
        Log out
      </Button>
    </Message.Header>
  </Message>
);

That looks like this when the user is signed in:

The component to sign in is taken care of for us by the StyledFirebaseAuth component provided by react-firebaseui. It uses the uiConfig object to decide what buttons to show.

const SignIn = () => (
  <StyledFirebaseAuth
    uiConfig={uiConfig}
    firebaseAuth={firebase.auth()}
  />
);

That's literally all we need to do to show a "sign in with Google" button that looks like this:

Controlling what users can do with courses

The course list component just passes the user along to each course component:

const CourseList = ({ courses, user }) => {
  ... 
  return (
    <React.Fragment>
      ...
      <Button.Group>
        { termCourses.map(course =>
           <Course key={ course.id } course={ course }
             state={ { selected, toggle } }
             user={ user } />) }
      </Button.Group>
    </React.Fragment>
  );
};

The only change we need to make in the course component is to provide the double-click action to change a course time if the user is null. So we change our code to test for the user and return the move course function only if the user is not null:

const Course = ({ course, state, user }) => (
  <Button color={ buttonColor(state.selected.includes(course)) }
    onClick={ () => state.toggle(course) }
    onDoubleClick={ user ? () => moveCourse(course) : null }
    disabled={ hasConflict(course, state.selected) }
    >
    { getCourseTerm(course) } CS { getCourseNumber(course) }: { course.title }
  </Button>
);

Our slice is done! We now have a working database and Google-based authentication. While there was a fair amount of code, almost all of it was for the user interface or business logic, not for managing changes in the database or user status.

Github Gist

Here's the Github gist for the complete App.js. This is not runnable code until you replace the dummy Firebase config object with an object for a real project.

Slice 8: Modularization

To keep things simple, we've put all our code in App.js. At this point, it's about 200 lines of code. It's overdue for refactoring into independent files. Refactoring dramatically simplifies code editing. There's less to look at when changing a file. There's less chance a team mate will be editing the same file, causing a merge conflict. There's more code that can be re-used in other projects.

Some authors recommending developing components in separate files from the start. For me, this introduces a fair amount of overhead when first developing an app from scratch. I prefer to only refactor when editing a file becomes an issue, just as I only worry about speed when an app becomes too slow.

React has few restrictions on how you organize your source code. Typically index.js and App.js will be at the top level. It's common to put files for component in a components subdirectory. Our recommendations below follow those made here.

When refactoring, focus on the big components. In our case, the course list component is an obvious candidate to pull out is the course list in App.js. We're going to go from this directory structure:

to this:

To do so is just a few steps:

  • Put all the code and imports that CourseList needs into the file src/components/CourseList.js. This will include the TermSelector component.
  • Add export default CourseList; to the end of CourseList.js.
  • Delete all that course list code from App.js. Remove any imports no longer needed in App.js.
  • Add import CourseList from './components/CourseList'; to the imports at the top of App.js.

Save your changes and verify that your code still works. Congratulations! You have done your first refactoring. The new App.js is less than 60 lines, and the new CourseList.js is less than 100 lines.

The next biggest component to refactor is the course component in CourseList.js. We'll change our directory structure to look like this:

Repeat the above process to create a src/components/Course.js file, and import it into the course list component file. Note that the App file will be unaffected.

Save and verify that everything still works. Now you should have a course list file and course file, each about 50 lines long.

Component subdirectories

To illustrate one more common component pattern, we'll move all the functions in Course.js that check for date and time conflicts into a separate times.js module. Since this code is only needed by the course component, a common approach is to make a component subdirectory rather than a file. Then we can store the time handling code in that subdirectory.

You will need to restart your local React server for this kind of change to take effect.

To do this:

  • Create the subdirectory src/components/Course.
  • Move Course.js into the new subdirectory.
  • Refactor the JavaScript functions for meeting times into a new file src/components/Course/times.js.
    • Add an export at the end of times.js to export just the functions that Course needs.
  • Create the file src/components/Course/index.js, with the line
    export { default } from './Course';

The last step is a shorthand way to import Course into index.js and then export it.

When the React build tool sees import ... from 'components/Course', it looks for either src/components/Course.js or src/components/Course/index.js. When it finds and loads src/components/Course/index.js, it gets Course. This behavior is one reason why you should not write:

import Course from 'components/Course.js';

Save and verify that your app still works. Now the course file is less than 20 lines long, and times.js is about 40.

You'll see applications that put component code directly into the index.js file. That works, but means that when you are editing multiple component files, all your tabs will say "index.js". Using files named for the component avoids that problem.

Further modularization would depend on your needs. A more actively edited file benefits from refactoring more than a file that's never touched.

Wrap Up

We've come to the end of this tour of modern React programming. We've seen functional components, styling, handling state, managing persistent data, and authentication. We've seen modern JavaScript techniques for defining functions, for destructuring and constructing objects. Both React and JavaScript emphasize small single-task components and functions, and minimal global state. The only global variables we used were for static constants.

Notice all the things we didn't need. Unlike a vanilla JavaScript or jQuery application, we didn't write any code to modify the page. We just updated state variables. Unlike previous versions of React, we didn't define any classes, we didn't define any lifecyle methods like componentDidMount or componentWillUpdate, and we never once worried about the this variable.

I've deliberately avoided many useful topics, such as higher order components, routing, and Redux. I've focused on the React and JavaScript concepts essential to begin writing clean manageable dynamic apps.

There's much more to learn, but it's time to explore on your own. For starting points, I recommend:

  • Do the Learn React task, to work through a more substantial example, with Firebase data access and updating and authentication. It's a good way to test your understanding of basic React ideas.
  • Take a look at React Old and New for discussion of how new React differs from old, since many resources online still use the old approach.

Happy Reacting!

© 2019 Chris Riesbeck
Template design by Andreas Viklund