Initial Due Date: 2025-02-27 9:45AM
Final Due Date: 2025-03-13 4:15PM
map
useState
hookđź’» git clone
(get the URL of the repository from GitHub). Read more about the process of creating and cloning your repository here.package.json
file and add your name as the author of the package.đź’» pnpm install
in the terminal in the root directory of your package (the directory that contains the package.json file).In this practical, you are going to put together a basic React app that shows the courses in the CS catalog. It looks like this:
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.
To run the development server, execute đź’» pnpm 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.
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 đź’» pnpm test
on the command lin (to run the tests and the development server at the same time you will likely need to open another terminal window). You can practice TDD as some tests will initially fail and then you can slowly get them to pass by following the directions below.
Changes in Node 22 are triggering deprecation warnings for the punycode
module. You can ignore this warning. We have tried to suppress this message but have only been partly successful doing so.
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 đź’» pnpm test
. For example, đź’» pnpm 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. 💻 pnpm 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
.
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>
.
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, 146, 150]}]
}
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.
Take a look at CourseEntry
. The skeleton provides a working implementation of the component with some basic functionality.
export default function CourseEntry({ course }) {
let summaryStyle = "";
return (
<details className={styles.description}>
<summary className={summaryStyle}>
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.
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.
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 courses
property in the props is assigned to the courses
variable.
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 (<></>);
}
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.
Now we can build 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 src/pages/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} />
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
. Inside the CourseList
component at the very top, 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
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).
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:
const items = courses.map((course) => (<CourseEntry key={course.number} course={course} currentCourse={currentCourse} setCurrentCourse={setCurrentCourse} />));
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;
...
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 Chrome).
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 example, 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. Look for a (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.
Bolding the entry you are hovering over does not require 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;
}
});
}
})
}
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 đź’» pnpm test
) and there are no linting errors (with đź’» pnpm run lint
). Once you have fixed any test or linting errors, add and commit any changes you may have made and then push your changes to GitHub. You should then submit your repository to Gradescope as described here.
If you attempt to push your changes to GitHub and get an error like the following, your repository on GitHub has commits that you local clone does not (as a result of how GitHub classroom seems to work, see details below). You will need to pull the changes you don’t have. In this case, you can safely and effectively use rebase to do so, i.e., execute 💻 git pull --rebase origin main
in the terminal. Then attempt to push again.
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:csci312a-s25/assignment01...'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
It appears that there can sometimes be a delay between when GitHub classroom creates your repository and when it finishes adding its automatic commits (for the due date, etc.). Thus is it possible (easy) to clone the repository before that process has completed and end up in a situation where the GitHub repository has commits your local clone does not.
Required functionality:
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).