CSCI 338: Fall 2023

Software Engineering

CSCI 338: Fall 2023

Assignments > Lab 7: Intro to Client-Side Programming with React

Due on Thu, 11/02 @ 11:59PM. 6 Points.

In Lab 6, you built a client-side web app using “Vanilla JavaScript.” In this lab, you will be building the same client using React.

1. Background Readings and Resources

Some useful React readings:

Other References (As Needed)

Slides & Video Recordings

2. Set-up

Tips before you begin

In this version of the task list app, you are using React and Parcel. React is a client-side framework that will ultimately need to be transpiled to HTML, CSS, and JavaScript. Parcel is a tool that helps you transpile / bundle your code everytime you save. Given this:

  • We recommend that you disable VS Code’s Auto Save feature for this lab, because you don’t want to activate your bundler on every keystroke (which may be invalid JavaScript…which could trigger transpiler errors that aren’t really errors).
  • Conversely, if you think the transpiler is confused and you want to trigger the build process again, just add a blank line to your code and save (all saves trigger the build process).

2.1. Create a lab07-your-username branch

After completing Lab 6, you will create a new branch from your existing lab06-your-username branch (from within your class-exercises-fall2023 repository) as follows:

1
2
3
4
git status  # make sure you've committed all of your files
git branch  # verify that you're on the lab06-your-username branch
git checkout -b lab07-your-username  # should create a new branch based on your lab05 branch
git branch  # verify that you're on your new branch

When you’re done, please make the following modifications to your code:

2.2. Modify existing files and add some new ones

In order to run your Task app using React, we’re going use two different Docker images – one for Python + Fast API, and one for Node.js + React. To do this, please open VS Code and make the following changes (within your lab07-your-username branch) to your version of lab05:

2.2.1. Dockerfile

Replace the contents of your current Dockerfile (within the lab05 directory) with this:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Image for Node.js and React:
from node:lts
COPY ./src/ui /ui
WORKDIR /ui
RUN npm run build

# Image for Python / Fast API
from python:3.11
COPY ./src /app
COPY --from=0 /ui/dist /app/ui/dist
WORKDIR /app
RUN pip install poetry
RUN poetry install

Note that there are now two images:

2.2.2. compose.yaml

Also in the lab05 directory – on the same level as your Docker file – create a new file called compose.yaml that contains the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
services:
  server:
    build:
      context: .
    ports:
      - "8000:8000"
    volumes:
      - ./src:/app
    depends_on:
      ui:
       condition: service_healthy
    entrypoint: poetry run uvicorn server:app --host 0.0.0.0 --reload

  ui:
    image: node:lts
    # user: "${UID}:${GID}"
    ports:
      - "1234:1234"
    volumes:
      - ./src/ui:/app
    working_dir: /app
    healthcheck:
      test: "ls dist"
      timeout: 90s
      interval: 10s
    entrypoint: npm run watch

The Docker compose file’s job is to provide instructions regarding how each container should be built from its respective image.

2.2.3. package.json

Inside of src/ui, create a new file called package.json with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "scripts": {
    "lint": "html-validate *.html && stylelint ./css/*.css && eslint ./js/*.js",
    "build": "npm install && parcel build index.html",
    "watch": "npm install && parcel watch index.html --no-hmr",
    "format": "prettier -w ./js/*.js"
  },
  "devDependencies": {
    "eslint": "^8.51.0",
    "eslint-plugin-react": "^7.33.2",
    "html-validate": "^8.6.0",
    "parcel": "^2.10.0",
    "prettier": "^3.0.3",
    "process": "^0.11.10",
    "stylelint": "^15.11.0",
    "stylelint-config-standard": "^34.0.0"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

2.2.4. Create an .eslintrc.json file

Inside of src/ui, create a new file called .eslintrc.json with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "settings": {
    "react": {
      "version": "detect"
    }
  },
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": ["eslint:recommended", "plugin:react/recommended"],
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "plugins": ["react"],
  "rules": {
    "react/prop-types": 0,
    "no-unused-vars": "off"
  }
}

2.2.5. Create a .stylelintrc.json file

Inside of src/ui, create a new file called .stylelintrc.json with the following code:

1
2
3
4
5
6
7
8
{
    "extends": "stylelint-config-standard",
    "rules": {
        "declaration-block-no-redundant-longhand-properties": null,
        "no-descending-specificity": null,
        "color-function-notation": null
    }
}

2.2.6. Update the .gitignore file

Since we will now be introducing React, some new folders will get created (node_modules, .parcel-cache, and dist). Because these directories contain code that has been generated by third-party modules, they are typically excluded from version control using the .gitignore file. Therefore, please open your .gitignore file (at the root of your lab05) and add the following lines:

1
2
3
node_modules
.parcel-cache
dist

2.3. Build New Docker Images & Container

You are now ready to build your Docker containers using the Docker compose command, which you will issue from within your lab05 directory (on the command line):

1
docker compose up

Unlike before, the shell processes runs in the foreground (which is actually a good thing because you can debug!). When the installation and build scripts finish, you should see an output line that says something like:

1
Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

Your server is now ready to be tested. Please verify that your old “vanilla” client is running: http://localhost:8000/index.html

Two Docker Images, Two Containers

Notice that in your Docker Dashboard, there are two images (one for node and one for python) and two containers. Pretty neat! This means that if you need to issue any node commands, hop onto the task-list-labs-ui container. Similarly, if you need to issue any python commands, hop onto task-list-labs-server container. Reminder of how to get to the shell prompt for each:

1
2
docker container ls -a
docker exec -it <container_id> sh

2.4. Setup your app to work with React

Finally, now that your new Docker instance is configured, you’re ready to rewrite your code using react. Please make the following changes:

2.4.1. Server-Side Updates

In server.py, change your mount point to ui/dist (which is the directory where your bundled client-side files will live). This line should be at the very bottom of server.py:

1
2
# app.mount("/", StaticFiles(directory="ui", html=True), name="ui")
app.mount("/", StaticFiles(directory="ui/dist", html=True), name="ui")

By changing this path, you’re telling Fast API that all of your client-side files will come from the bundled version of your files created by Parcel (which happens to be ui/dist).

2.4.2. Client-Side File Updates

index.html

Rename your current index.html file (in ui) to index-vanilla.html. Then create a new, blank, index.html file in the same folder. When you’re done, add the following code to index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./css/tasks.css">
    <script type="module" src="./js/main.js" defer></script>
    <title>React Client</title>
</head>

<body>
    <div id="app"></div>
</body>

</html>
main.js

Rename your current main.js file (in ui/js) to main-vanilla.js. You’ll be creating a new React version of main.js in the next section.

2.5. Test your formatter and linter

From the command line on your laptop (or on the UI Docker container), navigate to the src/ui directory and run the linter and formatter:

1
2
npm run format  # will automatically fix your JavaScript formatting
npm run lint    # will check your HTML, CSS, and JavaScript formatting

You are now ready to build your React app.

3. Implement Your React Application

React and other client-side frameworks offer many convenient features including:

3.1. Create Component Stubs

For our React version of our “Task Client”, we’re going to break up our functionality into 4 components:

Before we implement all of the server requests and event handlers, let’s first create stubs for each of our components (just the JSX).

3.1.1. Form.js

The Form component’s job is to provide a way to:

Screenshot of a form

Let’s beging by creating a Form.js file inside of the ui/js directory. Paste the code shown below into Form.js, which returns a JSX representation of the form:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { React, useState } from "react";
export default function Form() {

    return (
        <form className="add-task" method="post">
            <h2>Add New Task</h2>
            <label htmlFor="name">Name:</label>
            <input
                type="text"
                placeholder="Task name"
            />
            <label htmlFor="description">Description:</label>
            <input
                type="text"
                placeholder="Task description"
            />
            <button>Add</button>
        </form>
    );
}

We’ll handle the communication logic later.

3.1.2. Task.js

The Task component’s job is to:

Screenshot of a task

Create a Task.js file inside of the ui/js directory. Paste the code shown below into Task.js, which returns a JSX representation of a task with hardcoded data.

1
2
3
4
5
6
7
8
9
10
11
12
import { React } from "react";

export default function Task() {

    return (
        <div className="item">
            <strong>Dishes</strong>
            <p>Do the dishes.</p>
            <button>Delete</button>
        </div>
    );
}

We’ll handle the communication logic later.

3.1.3. TaskList.js

The TaskList component’s job is to:

Screenshot of a task list component

Create a TaskList.js file inside of the ui/js directory. Paste the code shown below into TaskList.js, which returns a JSX representation of a list of tasks.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { React } from "react";
import Task from "./Task";

export default function TaskList() {

    return (
        <div className="task-list">
            <Task />
            <Task />
            <Task />
        </div>
    );
}

Note that we’ve hardcoded 3 tasks in this component (which will change when we start fetching data from the server). We’ll modify this component later so that it’s data-driven.

3.1.4. App.js

The App component’s job is to:

Screenshot of a app component

Create an App.js file inside of the ui/js directory. Paste the code shown below into App.js, which returns a JSX representation of a list of tasks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { React, useEffect, useState } from "react";
import Form from "./Form";
import TaskList from "./TaskList";

export default function App() {
    return (
        <main>
            <section>
                <h2>Task List</h2>
                <TaskList />
            </section>
            <Form />
        </main>
    );
}

Note that App imports Form and TaskList. We’ll figure out the communication logic, including who passes which message where.

3.1.5. main.js

Finally, create a main.js file inside of the ui/js directory, which will “kick off” your React application. Then, paste in the following code:

1
2
3
4
5
6
7
8
9
10
11
import { React } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

function main() {
    const root = createRoot(document.getElementById("app"));
    root.render(<App />);
}

// Invoke the function that kicks off React!
main();

This code reaches into the DOM (which is initialized via index.html), and appends our React app to the DOM element with an id of “app”.

3.1.6. Verify that everything works

After making the changes described above, please run your new react app: http://localhost:8000/index.html. You should see a screen that looks like this:

Screenshot of a app component

Note that none of the functionality has been implemented yet.

3.2. Making the Task component data-driven

Our next step is to display our tasks based on data returned from the server. But, before we do this, let’s jazz-up our Task component so that the task name and description aren’t hard-coded. To do this, we’re going to make use of props.

In Task.js, please make the following changes:

  1. Pass a name and description props into the Task component function
  2. Replace the hard coded data with the name and description expressions:
1
2
3
4
5
6
7
8
9
10
11
import { React } from "react";

export default function Task({ name, description }) {
    return (
        <div className="item">
            <strong>{name}</strong>
            <p>{description}</p>
            <button>Delete</button>
        </div>
    );
}

In TaskList.js, please make the corresponding changes:

1
2
3
4
5
6
7
8
9
10
11
12
import { React } from "react";
import Task from "./Task";

export default function TaskList() {
    return (
        <div className="task-list">
            <Task name="Dishes" description="Do the dishes" />
            <Task name="Trash" description="Take out the trash" />
            <Task name="Lawn" description="Mow the lawn" />
        </div>
    );
}

Take a look at http://localhost:8000/index.html. If you did it correctly, your screen should look like this:

Screenshot of a app component

Congratulations! Your Task Component can now accept data from its parent.

3.3. Fetching data from the server

Hint: Ensure that you have some predefined tasks in server.py

Before completing this section, you may want to add some dummy tasks to server.py (if you haven’t already). See Lab 6 for a suggested starter taskdb.

Next, we need to display tasks from the server, so we’ll need to issue a GET request to /tasks. For now, let’s do this in App.js and pass the resulting data to the TaskList component. This may or may not be the “right” choice, but we can always switch things around if we need to.

We’ll fetch data the usual way, but instead of manually updating the DOM (like in Lab 6), we’ll create a state variable to do our work for us. Take a look at the following code snippet:

1
2
3
4
5
6
7
8
const [taskData, setTaskData] = useState([]);

async function fetchTasks() {
    const response = await fetch("/tasks");
    const data = await response.json();
    console.log(data); // for debugging
    setTaskData(data);
}

To invoke our fetchTasks function safely, we need to use a useEffect as follows:

1
2
3
4
useEffect(function () {
    // invoke side effects function safely:
    fetchTasks();
}, [])

This will ensure that fetchTasks() is only called once (and not every time the component re-renders…which happens every time a state variable is set…which would cause an infinite loop).

Finally, we need to pass the resulting taskData to the TaskList component so that TaskList can generate all of the tasks:

1
<TaskList taskData={taskData} />

To summarize, your App component should now look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { React, useEffect, useState } from "react";
import Form from "./Form";
import TaskList from "./TaskList";

export default function App() {

    // state variable initialized:
    const [taskData, setTaskData] = useState([]);

    async function fetchTasks() {
        const response = await fetch("/tasks");
        const data = await response.json();
        console.log(data);
        setTaskData(data);
    }

    useEffect(function () {
        // invoke side effects function safely:
        fetchTasks();
    }, []);

    return (
        <main>
            <section>
                <h2>Task List</h2>
                <TaskList taskData={taskData} />
            </section>
            <Form />
        </main>
    );
}

3.4. Displaying server data in TaskList

Now that you’ve got some data to display and your App component is passing this data to TaskList, we need to modify TaskList so that it displays the task. To do this, we’ll make two changes.

First, we’ll update the TaskList function signature to accept the new taskData prop:

1
2
3
export default function TaskList({ taskData }) {
    ...
}

Second, we’ll remove the hard-coded tasks and use JavaScript’s built-in map function to loop through each task and generate a list of Task components. The Array.map function takes a list and performs a data transformation on each element of the list. “I’ll give you a list of JSON objects, and you’ll give me back a list of Task components”:

1
2
3
4
5
6
7
8
9
10
<div className="task-list">
    {taskData.map((task, idx) => (
        <Task
            idx={idx}
            key={idx}
            name={task.name}
            description={task.description}
        />
    ))}
</div>

One other change to note here: Because a user is allowed to delete any task, we need to keep track of the position (idx) of the task and pass it to the Task component.

Your TaskList.js file should now look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { React } from "react";
import Task from "./Task";

export default function TaskList({ taskData }) {
    console.log("TaskList", taskData);
    return (
        <div className="task-list">
            {taskData.map((task, idx) => (
                <Task
                    idx={idx}
                    key={idx}
                    name={task.name}
                    description={task.description}
                />
            ))}
        </div>
    );
}

Take a look at http://localhost:8000/index.html. If you did it correctly, your screen should look something like this (but with your data from server.py):

Screenshot of a app component

Congratulations! You should now be viewing tasks that originated from the server.

3.5. Deleting Tasks

Recall that the code from section 3.4. is now passing in a new prop to the Task component – idx. We’re going to use this prop to know which task to delete.

To implement our delete functionality, you’ll first need to update the Task function signature to accept the new idx prop:

1
2
3
export default function Task({ idx, name, description }) {
    ...
}

Next, you’ll create a deleteTask function that will issue a DELETE request to /tasks/<id> as follows:

1
2
3
4
5
6
7
async function deleteTask() {
    const response = await fetch(`/tasks/${idx}`, {
        method: "DELETE",
    });
    const data = await response.json();
    console.log(data);
}

Then, you’ll attach the deleteTask function to the click event of the button:

1
<button onClick={deleteTask}>Delete</button>

Your Task.js file should now look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { React } from "react";

export default function Task({ idx, name, description }) {
    async function deleteTask() {
        const response = await fetch(`/tasks/${idx}`, {
            method: "DELETE",
        });
        const data = await response.json();
        console.log(data);
    }

    return (
        <div className="item">
            <strong>{name}</strong>
            <p>{description}</p>
            <button onClick={deleteTask}>Delete</button>
        </div>
    );
}

Navigate to your browser (with the console open) and try deleting one of your tasks. Then refresh your browser. The task should be gone. However, your screen didn’t redraw! We need to fix this!

3.6. Notifying TaskList that a task has been deleted

We need a way for Task to tell the App to re-fetch the tasks from the server and redraw them. To do this, Task needs access to the fetchTasks function we made in App. But how? The answer is that we need to pass the fetchTasks function definition as a prop! Please make the following changes:

App.js

Pass fetchTasks as a prop to TaskList:

1
<TaskList taskData={taskData} fetchTasks={fetchTasks} />

TaskList.js

Modify the TaskList signature to include the fetchTasks prop:

1
2
3
export default function TaskList({ taskData, fetchTasks }) {
    ...
}

Pass fetchTasks as a prop to Task:

1
2
3
4
5
6
7
<Task
    idx={idx}
    key={idx}
    name={task.name}
    description={task.description}
    fetchTasks={fetchTasks}
/>

Task.js

Modify the Task signature to include the fetchTasks prop:

1
2
3
export default function Task({ idx, name, description, fetchTasks }) {
    ...
}

Invoke fetchTasks after the delete happens (which will redraw the screen):

1
2
3
4
5
6
7
8
async function deleteTask() {
        const response = await fetch(`/tasks/${idx}`, {
            method: "DELETE",
        });
        const data = await response.json();
        console.log(data);
        fetchTasks(); // new
    }

Take a look at http://localhost:8000/index.html and try to delete some tasks. If it worked, the screen should be redrawn to reflect the deleted task.

Pro Tip

To get your tasks to come back, add a blank line to server.py and save it (which will trigger the server’s hot reload and restore the original dummy tasks).

3.7. Creating a new task

Last but not least, we’re going to create a way to add a new task to the server by issuing a POST request to /tasks. Open Form.js and make the following modifications (which aren’t exactly intuitive):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { React, useState } from "react";

export default function Form() {
    const [name, setName] = useState("");
    const [description, setDescription] = useState("");

    async function handleSubmit(ev) {
        ev.preventDefault();

        const response = await fetch("/tasks", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                name: name, // read from state variable
                description: description, // read from state variable
            }),
        });
        const data = await response.json();
        console.log(data);
    }

    return (
        <form className="add-task" method="post" onSubmit={handleSubmit}>
            <h2>Add New Task</h2>
            <label htmlFor="name">Name:</label>
            <input
                type="text"
                placeholder="Task name"
                value={name}
                onChange={(e) => setName(e.target.value)}
            />
            <label htmlFor="description">Description:</label>
            <input
                type="text"
                placeholder="Task description"
                value={description}
                onChange={(e) => setDescription(e.target.value)}
            />
            <button>Add</button>
        </form>
    );
}

Some notes here:

Navigate to http://localhost:8000/index.html and try to add a new task. Then refresh your browser. You should see the new task. However, your screen didn’t redraw!

Your Final Job

Your final task is to figure out how to redraw the screen (w/o a browser refresh) when a new task is added. When you’re done, you are done with this lab. Congrats!

4. What to Turn In

Before you submit, ensure that your react version of the lab successfully reads, deletes, and adds tasks; and that the screen redraws to reflect any task data changes. When you’re done, please create a pull request with the fully implemented web client (which should be completed inside of your version of your lab05 folder).