Recording Github

Now we complete two halves of a whole using React to create a basic frontend.

console.log("Hello React");

Hosted by: Harsh Deep

Motivation

The 50% rule - also good artist advice

at least half of the time you spend drawing must be devoted to drawing purely for its own sake. Not to learn, not to improve, not to develop your skills, not even to apply what you’ve already learned. There are no restrictions on medium, no specific techniques you must use, no subject matter you must focus on.

Setup

All you need is NodeJS. Check to see if you have the latest version installed on your device by typing node -v on your terminal. This will let us install everything we need.

Workshop

We’ll be making a more live/interactive version of uiuc’s gpa calculator. With some changing cat pics from a public API.

Starter template

One of the things nodejs includes is npx, which lets us set up depencies and templates quite well. We’ll start off with creating the really popular create-react-app template.

$ npx create-react-app gpa-cat-calc

This will take a while to run depending on your system, but this sets up our react app quite nicely.

Once it’s done, we can start VSCode and the local dev server and we’re ready to go:

$ cd gpa-cat-calc
$ code .
$ yarn start

yarn is a tool that handles our build commands, servers, tasks and dependency management. We’ll be using it to start our server and add libraries as we need.

Go to localhost:3000 on your browser to make sure it works.

Our first component - Calculator

React is a framework that is meant for creating complex and real-time frameworks in a nice way. In the past we used to generate and render web pages on the backend side completely, since there wasn’t much need for interactivity/real time usages and browser’s didn’t have much processing capability. It was created by Facebook for their need of managing complex frontends with many interactive components.

The way we manage it is via creating Components that build up our application. Each Component is like a standalone element that takes some arguments to get created, and has it’s independent state variables and functionality. If you’re used to object oriented programming, you’ll find most of the ideas map quite well.

Let’s create our first one for showing the main calculator interface. In the src/ folder, create Calc.jsx:

import React from 'react';

function Calc() {
    return (
        <div>
            <h1>Get your GPA</h1>
        </div>
    );
}

export default Calc;

jsx is a special type of javascript that blends both html code and js, creating really seamless templating. To create a react component, all we have to do is create a function with the name of the component and it should return a single html element representing our UI element. Since we can usually only return one element, it’s common to wrap everything inside a <div>

The export default is to allow our component to be included in other files, and now let’s take advantage of that to include it in App.js. Let’s also remove a lot of the starter UI code they’ve inlcuded so that we can focus on our own elements.

import logo from './logo.svg';
import './App.css';
import Calc from './Calc'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Calc />
      </header>
    </div>
  );
}

export default App;

This file is our entry component of our entire frontend, and we’ll include all our components within it. The way we could just add it by <Calc /> is my favorite part of react, the components we create basically become custom HTML elements. And we can nest and compose them any way we like. This type of custom elements allows us to create abstraction and manage complexities of large UIs. React also uses this kind of abstraction to only update parts of the UI that needs to be updated, making it quite efficient.

App.js is rendered properly by index.js but you don’t have to worry too much about that (feel free to take a look later if you want, it’s not too complicated).

If we check our browser, we should see our new component rendered properly.

Form Elements

Having headings and composable content is great, but we want our frontend to actually do things. Otherwise we don’t really need to use something fancy like React.

return (
        <div>
            <h1>Get your GPA</h1>

            <label htmlFor="gpa">Enter your GPA so far:</label>
            <input type="number" id="gpa"></input>
            <br />

            <label htmlFor="hours">Enter your credit hours so far:</label>
            <input type="number" id="hours"></input>
            <br />
        </div>
    );

If you check it on the browser, you should see two number fields. But we want it to actually do things, we have to attach the form elements to our state. React allows us to do this by creating a useState hook which provides us with a way to access and set our component’s state.

import React, { useState } from 'react';
[...]
function Calc() {
    const [hours, setHours] = useState(0);
    const [gpa, setGpa] = useState(0);

The first part acts as a getter and the second part is a setter. As long as we use the setters, we’ll always get a nice live UI. The value in the brackets of useState is the default starter value for these.

Let’s also do something with these numbers. Let’s create a function that calculates our GPA:

    const calculateGpa = (hours, gpa) => {
        let totalHours = hours;
        let totalPoints = hours * gpa;
        const newGpa = (totalPoints / totalHours).toPrecision(3);
        return newGpa;
    }

We can link form items with our state using the onChange event and display it and the end. We can embed javascript code in html by using {}. Your code will look something like this:

function Calc() {
    const [hours, setHours] = useState(0);
    const [gpa, setGpa] = useState(0);

    const calculateGpa = (hours, gpa) => {
        let totalHours = hours;
        let totalPoints = hours * gpa;
        const newGpa = (totalPoints / totalHours).toPrecision(3);
        return newGpa;
    }

    return (
        <div>
            <h1>Get your GPA</h1>

            <label htmlFor="gpa">Enter your GPA so far:</label>
            <input type="number" id="gpa" onChange={e => setGpa(parseInt(e.target.value))}></input>
            <br />

            <label htmlFor="hours">Enter your credit hours so far:</label>
            <input type="number" id="hours" onChange={e => setHours(parseInt(e.target.value))}></input>
            <br />

            <h2>Your new GPA is {calculateGpa(hours, gpa)}</h2>
        </div>
    );
}

If you play around with this on the browser, you’ll see that it changes live with adjustment. This is React in action.

Course Component

However, our GPA isn’t just the previous semester, but new courses coming in. For this we can create a new component representing a single Course and maintain a list.

First let’s load in some samples classes and the UIUC grade scale (feel free to copy paste)

const [gpa, setGpa] = useState(0);
const [courses, setCourses] = useState([
        {
            name: "CS 125",
            hours: 4,
            qualityPoints: 4,
        },
        {
            name: "Calc 2",
            hours: 3,
            qualityPoints: 4
        },
        {
            name: "Calc 3",
            hours: 3,
            qualityPoints: 4
        }
    ]);

const uiucGrades = {
        'A+': 4.0,
        'A':  4.0,
        'A-': 3.67,
        'B+': 3.33,
        'B':  3.00,
        'B-': 2.67,
        'C+': 2.33,
        'C':  2.00,
        'C-': 1.67,
        'D+': 1.33,
        'D':  1.00,
        'D-': 0.67, 
        'F': 0.00
    }
[...]

Here are three courses and we’re tracking hours, quality points based on the conversion table and the name of the class. Now let’s create a new Course.jsx component that can display this quite similar to how we did it before with each course being a <tr> table row element.

import React from 'react';

const Course = ({course, possibleGrades}) => {
    const possibleLetters = Object.keys(possibleGrades);
    return (
        <tr>
            <td><strong>{course.name}</strong></td>
            <td>
                <label htmlFor="hours">Hours:</label>
                <input type="number" name="hours" value={course.hours} 
                    ></input>
            </td>
                
            <td>    
                <label htmlFor="grade">Grade: </label>
                <select name="grade" 
                    >
                    {possibleLetters.map(letter =>
                        <option value={letter}>{letter}</option>
                    )}
                </select>
            </td>
        </tr>
    )
}

export default Course;

One new thing is that our component function is actually taking arguments in the function. These are called the props (short for properties) of our component. Think of this as the constructor arguments of a react component.

Now we just have to render the table with our rows, for this we use the map function. In our Course.jsx

[...]
import Course from './Course'
[...]

            <label htmlFor="hours">Enter your credit hours so far:</label>
            <input type="number" id="hours" onChange={e => setHours(parseInt(e.target.value))}></input>
            <br />

            <table>
                {courses.map(course => 
                    <Course course={course} 
                    possibleGrades={uiucGrades} 
                    />
                )}
            </table>

If you load it up in your browser, you’ll see a generated table with all our sample courses.

Adding and Removing

This is great, but not everyone takes the same exact courses. What we need is allowing people to add their own and remove existing courses. For this we just have to create functions that can add and remove to form elements. We’ll use js map/filter to our advantage. The ... splat operator is also very useful while working on react applications, since it lets us spread out existing contents of data really nicely.

    const addCourse = (courseName) => {
        setCourses([...courses, {name: courseName, hours: 3, qualityPoints: 4}]);
    };

    const removeCourse = (courseName) => {
        const newCourses = courses.filter(course => course.name !== courseName)
        setCourses(newCourses);
    };

The key thing to note here is that we never modify existing state objects, we always create new things and use the setter for that. This is called immutability and it a requirement for making sure a lot of React works as it should. Don’t worry about it being too slow, React is quite smart about optimizing and rendering.

Now let’s link the add to a text box form:

...
function Calc() {
    const [hours, setHours] = useState(0);
    const [gpa, setGpa] = useState(0);
    const [newCourse, setNewCourse] = useState("");
    ...
    const handleAddForm = (e) => {
        e.preventDefault();
        addCourse(newCourse);
        setNewCourse("");
    }
    return (
        ...
            </table>
            <form onSubmit={handleAddForm}>
                    <label htmlFor="courseName">New Course:</label>
                    <input type="text" id="courseName" value={newCourse} onChange={e => setNewCourse(e.target.value)}></input>
            </form>
            <h2>Your new GPA is {calculateGpa(hours, gpa)}</h2>
        </div>
    );
}

We use useState to manage our new box, and handleAdForm to prevent a page refresh (normal with a form change) to add the new course and reset the box. Try it out to see if you’re able to add courses right on the web browser.

Deleting is a little more complex, since it’ll have to be managed by the child <Course> component. Managing your own component’s state is easy, but children can’t always access that, so we pass a function as a prop that let’s it trigger right. Sounds confusing? Let’s see it in action:

Let’s add it in Course.jsx first:

const Course = ({course, possibleGrades, handleRemove}) = {
    ...
            </td>

            <td>
                <button class="delete-course" onClick={e => handleRemove(course.name)}>X</button>
            </td>
        </tr>
}

And now pass it right in Calc.jsx.

<Course course={course} 
    possibleGrades={uiucGrades} 
    handleRemove={removeCourse}
/>

Basically the Course component doesn’t have to really understand the details of how to really handle remove, and so it takes on the functionality from the Calc component. This is another type of abstraction that React encourages, by passing functions down to deal with things. Feel free to go to the browser and play around with these elements.

At this point your complete Course.jsx should be:

import React from 'react';

const Course = ({course, possibleGrades, handleRemove}) => {
    const possibleLetters = Object.keys(possibleGrades);
    return (
        <tr>
            <td><strong>{course.name}</strong></td>
            <td>
                <label htmlFor="hours">Hours:</label>
                <input type="number" name="hours" value={course.hours} 
                    ></input>
            </td>
                
            <td>    
                <label htmlFor="grade">Grade: </label>
                <select name="grade" 
                    >
                    {possibleLetters.map(letter =>
                        <option value={letter}>{letter}</option>
                    )}
                </select>
            </td>

            <td>
                <button class="delete-course" onClick={e => handleRemove(course.name)}>X</button>
            </td>
        </tr>
    )
}

export default Course;

and Calc.jsx should be:

import React, { useState } from 'react';

import Course from './Course'

function Calc() {
    const [hours, setHours] = useState(0);
    const [gpa, setGpa] = useState(0);
    const [newCourse, setNewCourse] = useState("");

    const [courses, setCourses] = useState([
        {
            name: "CS 125",
            hours: 4,
            qualityPoints: 4,
        },
        {
            name: "Calc 2",
            hours: 3,
            qualityPoints: 4
        },
        {
            name: "Calc 3",
            hours: 3,
            qualityPoints: 4
        }
    ]);

    const uiucGrades = {
        'A+': 4.0,
        'A':  4.0,
        'A-': 3.67,
        'B+': 3.33,
        'B':  3.00,
        'B-': 2.67,
        'C+': 2.33,
        'C':  2.00,
        'C-': 1.67,
        'D+': 1.33,
        'D':  1.00,
        'D-': 0.67, 
        'F': 0.00
    }

    const calculateGpa = (hours, gpa) => {
        let totalHours = hours;
        let totalPoints = hours * gpa;
        const newGpa = (totalPoints / totalHours).toPrecision(3);
        return newGpa;
    };

    const addCourse = (courseName) => {
        setCourses([...courses, {name: courseName, hours: 3, qualityPoints: 4}]);
    };

    const removeCourse = (courseName) => {
        const newCourses = courses.filter(course => course.name !== courseName)
        setCourses(newCourses);
    };

    const handleAddForm = (e) => {
        e.preventDefault();
        addCourse(newCourse);
        setNewCourse("");
    }

    return (
        <div>
            <h1>Get your GPA</h1>

            <label htmlFor="gpa">Enter your GPA so far:</label>
            <input type="number" id="gpa" onChange={e => setGpa(parseInt(e.target.value))}></input>
            <br />

            <label htmlFor="hours">Enter your credit hours so far:</label>
            <input type="number" id="hours" onChange={e => setHours(parseInt(e.target.value))}></input>
            <br />

            <table>
                {courses.map(course => 
                    <Course course={course} 
                    possibleGrades={uiucGrades} 
                    handleRemove={removeCourse}
                    />
                )}
            </table>

            <form onSubmit={handleAddForm}>
                <label htmlFor="courseName">New Course:</label>
                <input type="text" id="courseName" value={newCourse} onChange={e => setNewCourse(e.target.value)}></input>
            </form>

            <h2>Your new GPA is {calculateGpa(hours, gpa)}</h2>
        </div>
    );
}

export default Calc;

Factoring in current Courses into GPA

Now that we can add and remove courses, let’s have the form elements update our state as well. Because the courses array is part of Calc’s state, we provide our Course component with functions for modifying the array. Similar to add and remove, we keep immutability in mind:

    const updatePoints = (courseName, newPoints) => {
        const newCourses = courses.map(course => 
            course.name === courseName ? {...course, qualityPoints: newPoints} : course
        );
        setCourses(newCourses);
    };

    const updateHours = (courseName, newHours) => {
        const newCourses = courses.map(course => 
            course.name === courseName ? {...course, hours: newHours} : course
        );
        setCourses(newCourses);
    };
    ... 
                <Course course={course} 
                    possibleGrades={uiucGrades} 
                    handleRemove={removeCourse}
                    handleHourUpdate={updateHours} 
                    handlePointUpdate={updatePoints}
                />

Like earlier, we use the courseName as an id and then only change what we need. React is quite smart and will only update the parts of the UI that changed. Like remove, we pass the functions down to the Course component.

In the Course component let’s attach it to form elements and accept it as a prop.

const Course = ({course, possibleGrades, handleRemove, handlePointUpdate, handleHourUpdate}) => {
    ...
                <input type="number" name="hours" value={course.hours} 
                    onChange={e => handleHourUpdate(course.name, parseInt(e.target.value))}></input>
                ...
                <select name="grade" 
                    onChange={e => handlePointUpdate(course.name, possibleGrades[e.target.value])}>
                    {possibleLetters.map(letter =>
                        <option value={letter}>{letter}</option>
                    )}
                </select>

Now let’s finish off the functionality by actually taking the courses into account for the gpa calculation. In Calc.jsx add the for loop to account for current GPAs:

    const calculateGpa = (hours, gpa) => {
        let totalHours = hours;
        let totalPoints = hours * gpa;
        for (let i = 0; i < courses.length; i++) {
            totalHours += courses[i].hours;
            totalPoints += courses[i].hours * courses[i].qualityPoints;
        }
        const newGpa = (totalPoints / totalHours).toPrecision(3);
        return newGpa;
    };

If you did everything right. Now the GPA display on the bottom should change based on any single form value change. Good for people trying to calculate things instantly. Our core functionality now works!

We’re done with our Course component functionality, at this point your Course.jsx file should be:

import React from 'react';

const Course = ({course, possibleGrades, handlePointUpdate, handleHourUpdate, handleRemove}) => {
    const possibleLetters = Object.keys(possibleGrades);
    return (
        <tr>
            <td><strong>{course.name}</strong></td>
            
            <td>
                <label htmlFor="hours">Hours:</label>
                <input type="number" name="hours" value={course.hours} 
                    onChange={e => handleHourUpdate(course.name, parseInt(e.target.value))}></input>
            </td>
                
            <td>    
                <label htmlFor="grade">Grade: </label>
                <select name="grade" 
                    onChange={e => handlePointUpdate(course.name, possibleGrades[e.target.value])}>
                    {possibleLetters.map(letter =>
                        <option value={letter}>{letter}</option>
                    )}
                </select>
            </td>
            <td>
                <button class="delete-course" onClick={e => handleRemove(course.name)}>X</button>
            </td>
        </tr>
    )
}

export default Course;

Side effects

Sometimes, the changes of our UI can’t be captured by state and event functions. This is where useEffect comes in. This can be used for UI state, animations, sending logs, API requests and more. Here we’re going to change the the title of the browser to reflect the calculated gpa.

In Calc.jsx, first we import it:

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

Now we write our effect function:

    useEffect(() => {
        document.title = `Current GPA: ${calculateGpa(hours, gpa, courses)}`
    }, [hours, gpa, courses]);

    return (...)

The syntax is giving in a function, and then an array of state dependencies. This tells react to run this effect code during the render cycle when any of these variables has a change. If we now check our browser, the heading should have our gpa and it will also update accordingly. Effects are called after the state updates have happened, so take take not to use setState inside an effect to prevent infinite loops.

React Hooks (useState and useEffect here) are really interesting and in depth topics, and I suggest going through the Learn more links at the bottom to understand it well. If this kind of state management seems to start to get tricky, check out Redux which provides a really neat way to deal with state.

Exercise for the reader: Try to figure out how to memoize the calculateGpa function so that we don’t run it twice.

Cat API

Another fun thing to add is cat pictures. Looking at grades can be quite stressful, so we’re going to add a cat pic every time someone adds a course.

First let’s install axios, a good async API request library:

$ yarn add axios

We’ll be making use of CatAPI with the route https://api.thecatapi.com/v1/images/search. We’ll pick the first result and extract the url from it. In your actual application, you can make use of far more complex functionality.

First let’s create a state:

// towards the start of the fn
const [catPic, setCatPic] = useState(""); // default pic

and an image element in the end:

<img src={catPic} alt="cute kitty" height="200"></img>

Now let’s add the axios function for dealing with this:

    const updateCatPic = () => {
        axios.get("https://api.thecatapi.com/v1/images/search")
            .then(response => {
                setCatPic(response.data[0].url)
            })
    }

    const handleAddForm = (e) => {
        e.preventDefault();
        addCourse(newCourse);
        setNewCourse("");
        updateCatPic();
    }

Now if you add any courses it will update the cat picture each time :)

Styling

Frontend isn’t just creating elements and giving it functionality, but also making it presentable and usable. We can achieve this via css. CSS is a format for web sites where we give a collection of rules for our content. React makes style sheets easy to include in our application through imports.

It’s not something you need to memorize, but something I just google all the time. The basic thing to understand

  • class in html points to a collection of html elements, in css we can set rules based on .className to target those.

  • id in html points to a unique html element, in css we can set rules based on #idName to target it.

  • We can target html element types in css by just putting their tag name as is. For example if I wanted to target paragraphs

The template code in App.js includes this line:

import './App.css';

which adds the styles from the file into the app. Let’s try changing the background color to Illini Blue in App.css

.App-header {
  background-color: #13294b; /* uiuc blue */
  ...

Similarly we can create css files for all our components.

Let’s make it for Course where we change the delete button to red.

We create the new Course.css:

.delete-course {
    color: red;
}

and import it:

import './Course.css'

As we already set up the class earlier in the form, now this rule applies to all delete course objects.

Similarly let’s add some element and id styling to Calc.jsx:

input {
    width: 4em;
    padding-left: 5px;
    padding-right: 5px;
}
  
#gpa-display {
    color: #E84A27;
}

and import it

import './Calc.css'

Now everything should look a bit nicer. Of course, your real project will need far more extensive sytyling to look nice, so make sure to check out the bottom links on learning html+css as well.

Final Code

You can find the final code on Github. Great job getting this far.

Learn more

Ideas

  • Some interesting frontend that has some dynamic content.

  • Try to make it in a way that the user never has to reload.

  • Hook in some API and do something interesting.

  • Create something that has live update mechanisms. That’s always interesting to work with.

  • See if you can add Typescript in interesting ways. This is more

Requirements

  • You should show it off in your README.md file, with animations/video and an explanation of the game.

  • Has to use a Open Source license via a LICENSE file

Contributors: Harsh, Maaheen