Practical: React Page
Initial Due Date: 2024-09-26 11:59PM Final Due Date: 2024-10-25 4:00PMGithub Classroom Gradescope
In this practical, you are going to put together a basic React app that shows the courses in the CS catalog.
Learning Goals:
- Create your first React page
- Create a list of components using `map`
- Utilize the `useState` hook
- Add some simple styling with CSS
- Commit and push your changes to GitHub
- Submit your repository to the Gradescope assignment
Prerequisite
- Create the git repository for your practical by accepting the assignment from GitHub Classroom. This will create a new repository for you with a skeleton application already setup for you.
- Clone the repository to you computer with
đź’» git clone
(get the URL of the repository from GitHub). - Open up the
package.json
file and add your name as the author of the package. - Install the module dependencies by executing
đź’» npm install
in the terminal.
Overview
When you’re done, your react app will look like this:
Getting situated
I’ve provided you with the framework of the application, which was created with the create-next-app
tool. For the moment, we will ignore many of the features of the Next framework. Our focus today is the index.js
file in src/pages
that is the root React component for your site and the components
directory in src
, where you will find the individual components.
The development server
To run the development server, execute đź’» npm run dev
in the terminal (or click the “play” button next to dev
in the NPM Scripts section VSCode). As we discussed in class, there is a certain degree of translation that needs to happen to make your code runnable in the browser. The development server will handle that processing in real time as you update your code and serve your web pages as if it were a normal web server.
Once the development server spins up, you can visit the site in your browser at http://localhost:3000.
Testing
I encourage you to test your code in two ways. Running the development server and interacting with the web page you are building is essential to really understand what is going on. However, I have also given you a collection of tests for your work that you can run with đź’» npm test
on the command line. You can practice TDD as some tests will initially fail and then you can slowly get them to pass by following the directions below.
When you start, the number of errors can be daunting, so you can focus your testing a little.
You can run just one test file by putting its name after đź’» npm test
. For example, đź’» npm test CourseEntry
will only run the tests in CourseEntry.test.js
. Notice that I didn’t specify the path or even the whole name. Jest will pattern match and run all test files that match. 💻 npm test Course
will run both CourseEntry.test.js
and CourseList.test.js
. You can certainly add the path to make this more specific.
You can also target individual tests inside of a file. You can add .only
after test
or describe
(right before the parentheses) to only run that test or that test suite. Alternately, you can skip a test or test suite by adding .skip
.
pages
Open up src/pages/index.js
. This is where we will put the “root” of the application. You will see that the JSX in this file matches what you can see in the development server. Try a simple interaction by adding the following below the <h1>
header, but within <main>
:
<h2>Don't Panic!</h2>
When you save the file, the contents displayed in your web browser should change. Once you have seen this, remove <h2>Don't Panic!</h2>
.
The data
We are including the course catalog data as a JSON file. You will find it in data/cs-courses.json
. If you open it up, you will see that it is an array of objects, one per class. The objects have properties for number
, name
, description
, and optionally, prerequisites
. Prerequisites are listed as an array of numbers. Example:
{
"number": 431,
"name": "Computer Networks",
"description":"Computer networks have had a profound impact on modern society. This course will investigate how computer networks are designed and how they work. Examples from the Internet as well as our own campus network will be discussed.",
"prerequisites": [200,315]
}
Some prerequisites can be satisfied with one of a collection of options. This is represented using another object with an or
property like this:
{
"number": 201,
"name": "Data Structures",
"description":"In this course we will study the ideas and structures helpful in designing algorithms and writing programs for solving large, complex problems. The Java programming language and object-oriented paradigm are introduced in the context of important abstract data types (ADTs) such as stacks, queues, trees, and graphs. We will study efficient implementations of these ADTs, and learn classic algorithms to manipulate these structures for tasks such as sorting and searching. Prior programming experience is expected, but prior familiarity with the Java programming language is not assumed.",
"prerequisites":[{"or": [145, 150]}]
}
The components
You are going to add two components to the application: CourseEntry
(which represents a single class), and CourseList
(which represents the entire list of classes). The skeleton contains starter code for both; you will find the relevant files in src/components
.
CourseEntry
Take a look at CourseEntry
. The skeleton provides a working implementation of the component with some basic functionality.
export default function CourseEntry({ course }) {
return (
<details className={styles.description}>
<summary>
CSCI {course.number} - {course.name}{" "}
</summary>
<p>{course.description}</p>
</details>
);
}
We make use of the details HTML tag, which is a less well known HTML tag. It is a simple interactive widget that shows a text summary which can expand to show more detail.
This is a simple details example (click me)
When you click it, it expands to show more, well... details. It is actually three separate tags. There is the main details
tag around everything. Then there is the summary
tag which contains the collapsed view. Finally, there is whatever is holding the details themselves, usually a p
tag.
<div>
<details>
<summary>This is a simple details example (click me)</summary>
<p>When you click it, it expands to show more, well... details. It is actually three separate tags. There is the main <code>details</code> tag around everything. Then there is the <code>summary</code> tag which contains the collapsed view. Finally, there is whatever is holding the details themselves, usually a <code>p</code> tag.</p>
</details>
</div>
Looking at the component, you can see that CourseEntry
takes in a single course as a prop (with the name course
). It then uses the values in that object to populate the JSX with data.
Javascript Note
The component function is expecting a single object, the “props”, as an argument. By writing the function definition as function CourseEntry({ course }) {...
, we are “destructuring” that single object argument into individual variables by name, i.e., the values of the course
property in the props is assigned to the course
variable.
CourseList
The CourseList
component is not as far along. You can see that the component takes in a prop called courses
(the full list of course objects), but that is all that is included in the skeleton.
export default function CourseList({ courses }) {
return (<></>);
}
JSX Note
The empty tags are a convenience in JSX that allow us to have components that are just placeholders (we can also use them to get around the “can only return one element” rule). They don’t show up on the web page itself.
Make a list
Now we can flesh out CourseList
. We need to transform the array of course objects into an array of CourseEntry
components. Hopefully you immediately thought, map
! Before the return statement, we are going to build the list of CourseEntry
components. The basic structure will look like this:
const items = courses.map((course) => ());
What should that inner function return? The CourseEntry
component we want to render for that course. We aren’t restricted to using JSX only in return statements, we can put them anywhere where it makes sense to be talking about creating component. Here are we are creating an array of CourseEntry
components.
const items = courses.map((course) => (<CourseEntry />));
To use CourseEntry
in this component, we need to import its definition from the file where it is defined:
import CourseEntry from "./CourseEntry";
Now that we have the component imported, we can return to creating our CourseEntry
components. We need to give the CourseEntry
the course we want it to display, so we will add that as a prop (i.e., within the angle brackets). Recall that we specify props by name and provide the values in curly brackets. Anything within the curly brackets in JSX is Javascript code, in this case a reference to the course
parameter.
const items = courses.map((course) => (<CourseEntry course={course}/>));
When we make a list of React components, it is important that we add a key
property so that React can uniquely identify individual child components during rendering. When React is diff’ing the virtual DOM and the actual DOM, it needs to have a quick way to tell which component(s) has changed. Without a key, React falls back on simple ordering, and weird things happen if you delete an element out of the middle of a list. The React documentation on lists goes into more detail. The key can be anything, provided it is a unique identifier. In this case, course number will work well (though if we incorporated courses from other departments we would want to use the full name, e.g., “CSCI312”). Note that although key
is specified like other props, it is not passed through to the component, it is one of several properties used by React itself.
const items = courses.map((course) => (<CourseEntry key={course.number} course={course}/>));
Now that we have the items, we want to return them. We can only return a single component, so we need a wrapper around all of our CourseEntry
components. In this instance, a div
is a good choice. Change the return statement to return a div
element. To put the list of CourseEntry
components into the div
, you just need to stick the items
variable in there (remembering to surround it with curly braces since it is JavaScript). We are going to style the div
a little, so add className={styles.listContainer}
as an attribute of the div
tag (we will learn more about this shortly).
return (<div className={styles.listContainer}>{items}</div>);
To see the list, you need to put it on the page. Return to index.js
. First we need to import the CourseList
component. At the top of the page, add an import statement.
import CourseList from "../components/CourseList";
While you are doing that, take a moment to notice that the JSON file holding the course data has been imported as the variable courses
. This is a little trick performed by the development tools where the JSON is parsed into JavaScript if we import it like this. Add a CourseList
component to the page and pass it the courses
as a prop like shown below. Recall that we can create React components in JSX, just like we do HTML tags. You should now have a list of courses on your web page.
<CourseList courses={courses} />
Showing Prerequisites
We are going to add a little bit of interaction to the page. As the user hovers over different courses, we are going to highlight the courses that serve as prerequisites. Since all of the CourseEntry
components will need to see which entry is being hovered over, we are going to need a piece of state that lives above all of them in the hierarchy (i.e., in CourseList
).
Let’s call this piece of state currentCourse
. At the top of the CourseList
component, add:
const [currentCourse, setCurrentCourse] = useState(null);
This gives us our state variable (currentCourse
) and the method for setting it (setCurrentCourse
). We passed an initial value of null
to set the default course selection as no selection.
We need to import useState
to use it, so put the following line at the top of the file. The curly brace notation is how we import items that are exported from their modules, but that aren’t the default export.
import { useState } from "react";
Import Note
import
is an example where reading the documentation can prevent trouble later. import
, and the differences between default and non-default exports, is a frequent source of bugs throughout class. I suggest reviewing the different import styles, when and how they are used, and being careful to use the correct style for each file (don’t rely on the IDE to get it right).
Push the props down
We need to get the state information to the CourseEntry
components, so we need to add some new props. In the CourseList
component, where you create the CourseEntry
components, add a currentCourse
property and a setCurrentCourse
property to the CourseEntry
component and set their values appropriately (i.e., using the values created by useState
). This will tell each CourseEntry
component what the current course is and also provide a way to change it if the component needs to. Finish adding the new properties by going to the CourseEntry
component and adding them to the destructured props
argument. Do not use different names for the props as it will break the automated tests.
More generally, adding props to an existing component is typically a two-step process:
- We add the prop where we render the component (i.e., in its parents)
const items = courses.map((course) => (<CourseEntry key={course.number} course={course} currentCourse={currentCourse} setCurrentCourse={setCurrentCourse} />));
- We add the props to the destructured object argument in the component itself. Recall that React components actually receive a single argument, the props object. To be more concise, we typically destructure that object in the function definition, i.e.,
function CourseEntry({ course, currentCourse, setCurrentCourse }) {
which is just “syntactic sugar” for
function CourseEntry(props) { const course = props.course; const currentCourse = props.currentCourse; const setCurrentCourse = props.setCurrentCourse; ...
Changing the current course
We want the current course to change when we hover over it. One of the events we can listen for is the cursor moving over an element on the page. We register a callback for it with onMouseEnter
, which fires the moment the cursor enters the region defined by the component.
When the cursor enters the region of our details
component in CourseEntry.js
, we want it to call setCurrentCourse
with the course associated with the component. Add the onMouseEnter
listener to the details
as shown below.
<details className={styles.description} onMouseEnter={() => setCurrentCourse(course)} >
Now, as the user mouses over the list, the listener will “fire”, invoking the setCurrentCourse
callback to update the current course. To see this in action, add a console.log(currentCourse)
to the CourseList
component and then open up the console tab in the browser developer tools (documentation for finding the developer tools in Chrome).
Add some styling
Printing things to the console is useful for debugging, but we want users to be able to see what they are doing. So, we will change the styling so that the summary of the current course is bolded.
We are using CSS modules for this practical, so you will find the stylesheet for CourseEntry
in src/styles/CourseEntry.module.css
. I would like you to add a new class in that file that sets the font-weight
to bold
(if you aren’t sure what this says, please read up on CSS).
.current {
font-weight: bold;
}
Now we need to apply this CSS class to the summary
element in CourseEntry
. To access a class from a CSS module, we import the module (which I’ve already done for you), and then we can refer to the class name as styles.class-name
(where styles
is determined by the import). So, in this instance, it will be styles.current
.
We only want the style to applied when the course we are rendering is actually the current course, so we are going to set the value conditionally. Create a new variable called summaryStyle
. If the course
is equal to currentCourse
, set the variable to styles.current
(don’t forget to use ===
for the equality check), if not set it to ""
(the empty string).
Add a className
attribute to summary
and set it to summaryStyle
, e.g, <summary className={summaryStyle} ...>
. The courses should now show up in bold as you mouse over them.
Add styling for prerequisites
Bolding the entry you are over hardly requires state (we could actually do that with pure CSS). We defined state because we are going to also show the prerequisites. Go back to the CourseEntry.modules.css
CSS module, and add the following styling for prerequisites:
.prereq {
font-weight: bold;
background: lightslategray;
color:white;
}
Now, back in CourseEntry
, we are going to add some more code to check if this course is a prerequisite for the current course. To so we need to check if the course we are rendering is a prerequisite of the current course (either in the pre-requisite list or for an “OR” prerequisite, in the sub list). If the course is a prerequisite of the currentCourse
, then we will set the style of the summary
element to our new prereq
style. A potential implementation is:
if (currentCourse && currentCourse.prerequisites) {
currentCourse.prerequisites.forEach((req) => {
if (req === course.number) {
summaryStyle = styles.prereq;
} else if (req.or) {
req.or.forEach((altreq) => {
if (altreq === course.number) {
summaryStyle = styles.prereq;
}
});
}
})
}
Finishing up
While this isn’t the most complex web application, you now have an example of the React development process and it’s component-based approach. Note how we implemented the interactivity. We didn’t need to specify how to change the web page when the user hovers over a different course. Instead we specified how the site should render given the currentCourse
state and how to change that state in response to user actions. React fills in the rest. Building a more sophisticated, even more interactive, application is the same process, just with more and more complex components.
Make sure the tests are passing (with đź’» npm test
) and there are no linting errors (with đź’» npm run lint
). Once you have fixed any test or linting errors, commit any changes you may have made and then push your changes to GitHub. You should then submit your repository to Gradescope.
Grading
Required functionality:
- Should display all courses
- Hovered over courses should be bolded
- Pre-requisites should by highlighted
- Pass all tests
- Pass all ESLint checks
Recall that the Practical exercises are evaluated as “Satisfactory/Not yet satisfactory”. Your submission will need to implement all of the required functionality (i.e., pass all the tests) to be Satisfactory (2 points).