Assignment Three
October 09, 2025 at 11:59 PM
Goals
- Get more practice implementing React components
- learn about static typing by converting your code to TypeScript
- Advance your understanding of the division of responsibilities and state between components
Prerequisites
This assignment builds on the work of assignment 2. As such, you should not start it until you have passed most, if not all of the tests for assignment 2.
- Click the GitHub Classroom link and then clone the repository GitHub classroom creates to your local computer (in your shell, execute
git clone
followed by the address of the repository). - Update the
package.json
file with your name and e-mail - Install the package dependencies with
pnpm install
Once you have the dependencies installed you can start the development server with pnpm run dev
.
Background
This assignment, the next part of creating Simplepedia, starts where assignment two left off. As with previous assignments an initial set of (failing) tests are provided as part of the skeleton (run with pnpm test
). These tests are intended to help you as you develop your application and to ensure that the grading scripts can successfully test your assignment. Code that passes all of the provided tests is not guaranteed to be correct. However, code that fails one or more these tests does not meet the specification. You are not expected to write any additional tests for this assignment.
As a general bit of advice, worrying too much about the flow of interactions through your application can lead you astray. While information flow is important, focusing on it will have you tied in knots trying pass information between components. Practice compartmentalization and trusting that other components will work. Recall the philosophy of React. Focus on “given the current state and props, what should I render?” and “how do I update that state in response to user actions”. React will take care of triggering the necessary re-rendering.
Assignment
Part 0: Port over Assignment 2
The first thing that you need to do is to update the Assignment 3 skelton with the code you wrote for assignment 2. Be a little cautious as you do this – there are some small changes and you don’t want to just replace the files with your old ones.
Some specific changes to keep an eye out for: 1. The name of the Simplepedia
component has been changed to SimplepediaArticle
1. The name of the Article
component has been changed to ArticleView
1. The article collection is now a prop of SimplepediaArticle
instead of being state. If you take a look in _app.js
you will find the data’s new home. 1. What will become common layout (e.g., title, footer) has been pulled into _app.js
to reduce duplication in the other components.
.
Make sure that your code has all of the functionality from assignment 2 before proceeding. Note that some of the tests are the same, but many are testing for the new functionality and you many need to use skip
and only
to prune down to tests that only test the old behavior.
Part 1: Switch to TypeScript
TypeScript checking is now enabled for this assignment. To get your file checked, rename it from .jsx to .tsx. VSCode should then issue warnings and you can also check all TS files by running pnpm run typecheck
.
I have provided an interface that describes the Article
type in src/types/Article.tsx. You should use this wherever you need an article.
Work slowly, one file at a time. Your primary concern should be creating type descriptions for the props. Most other things will be handled by the TS type inference. However, if the type checker complains you should add type annotations to make it work.
Make sure that by the end you have converted all .jsx files to .tsx files and that pnpm run typecheck
tells you everything is properly checked.
You will get a collection of errors from the test files, especially the ones testing files you haven’t created yet. You can ignore those.
Part 2: Switch to routing (requirements change!)
As I mentioned in class, Simplepedia has a critical limitation – you can’t bookmark a particular article or share a link to it. We are going to fix that with routing. (You can read about Next routing in the Next documentation, but read through everything here first).
Big Picture After the change, every article will now have a unique URL. For example, the article on ‘Pierre Desrey’ will now be located at http://localhost:3000/articles/42. Rather than storing a currentArticle
as state, we will the use the URL to keep track of which article we are looking at (if any).
We will use a “RESTful” API with the following initial URLs:
URL | View |
---|---|
/articles/ |
Main entry point showing sections, but with no section or article selected. |
/articles/[id] |
Show article with id and corresponding section. |
Dynamic routes
In class, we discussed Next’s dynamic routes, which allow us to introduce variables into the route that are available to our code.
As a reminder, we specify a dynamic route by putting the name in square brackets. We are going to use a special form called an “optional catch all dynamic route”. Don’t worry too much about understanding the details here – just try to keep up with the instructions.
- Make a directory within
pages
, namedarticles
(recall routes are determined by the file structure within thepages
directory). - Move your current
index.js
to be a file named[[...id]].js
, within thearticles
directory. This will now match/articles
,/articles/42
, etc. At this point (assuming you have the development serving running) you will likely see a 404 error in the browser. Try updating the URL to the http://localhost:3000/articles. Your application should still work. - Open up next.config.ts. Inside, you will find some commented directives starting with
async redirects()
. Uncomment all of it. This sets up a redirect so that anyone who tries to visit http://localhost:3000/ will be redirected to http://localhost:3000/articles.
The id
in there is a variable, and we would like to know what it is so we can display the right article. To do that, we are going to use the useRouter
hook to access the React router state. In anticipation of adding a second page, we are going to actually read read off the article id in _app.js
, so switch to that file.
Within _app.js
: 1. Import useRouter
with import { useRouter } from "next/router";
1. Add const router = useRouter();
to the top of the component 1. Access the id
variable with const { id } = router.query;
(note the destructuring assignment) in the body of the MainApp
function.
- import
useRouter
withimport { useRouter } from "next/router";
- Add
const router = useRouter();
to the component - access the
id
variable withconst {id} = router.query;
(note the destructuring assignment)
Try console logging the id
to make sure changing the URL works before moving on
Recall that your React components are executing in the browser, so any console.log
statements in your components will actually print to the browser’s console (accessible via the browser’s Developer Tools).
Determine (and update) currentArticle
from id
Create a new currentArticle
variable in MainApp
. Use the id
to look up the article in the collection. The find
function would be a good choice.
There are two things to watch out for:
- Sometimes
id
will beundefined
(e.g., when there is no id), in which case just set yourcurrentArticle
variable to undefined. id
will be a string, while the ids of the articles will be numbers, and they will need to be the same type if you use===
. As a hint, the unary+
operator can be used to convert strings to numbers, e.g., try+"42"
in the node interpreter.
You will also need a a way to change the current article. Create a function within MainApp
called serCurrentArticle
that will be used as the setter for the current article. Note that this will not change the variable directly – it will update the URL programmatically.
To change the URL we will use the router.push()
method with the route we wish to visit as the argument. Thus to view the article with the id
of 42, we would invoke router.push("/articles/42")
. If we want to clear the current article (i.e., someone calls the function with no arguments), we can call router.push("/articles")
.
The router exposes a push
method because we think about the sequence of sites that we visit as a stack of pages. The “Back” button is essentially popping the last page off of the stack revealing where we were immediately before.
Now that you have currentArticle
and setCurrentArticle
, add them to the props
object in MainApp
, which will pass it along to all pages.
In the SimplepediaArticle
component, you will want to add both as expected props. At this point you can get rid of the original currentArticle
state you created with useState
.
If you originally used a different name for that state than currentArticle
and setCurrentArticle
, update those names to be consistent with the new props you added
This should restore the browsing functionality back to the application and you should be able to navigate around the different articles.
Part 3: Allow the user to add new files
You will find a new Editor
component in src/components/Editor.tsx. It should allow the user to create a new article (note that this is just the form to enter in a new article – it doesn’t handle the actual storage). The Editor
component takes two props:
currentArticle
- the current article, we wil ignore this for the momentcomplete
- a function thatEditor
will call with the new article when editing is done
Make sure not to change these names, or any others described in this assignment to facilitate automated testing. While there may be different, equally valid designs, you are expected to code to these specifications.
Your component should use an <input>
of type “text” to allow the user to enter in a new title and a <textarea>
to allow the user to write the body of the article. Your component should have two buttons. The first should be a “Save” button. When the user clicks it, the article should be saved, and editing should be completed (your editor styling does not need to match the example above). The date on the article should be set to the current date and time (as an ISOString via Date.toISOString()
), and the article should be added to the collection. The second should be a “Cancel” button. When the user clicks cancel, the edit should be discarded and the primary interface should be restored. Keep in mind the newly created article might not belong to an existing section.
There is one form validation required. If the title is not set, you should not let the user save the article by disabling the “Save” button. UX best practices are to also provide a message explaining the validation error (as close in time and space to the error as possible), however for simplicity in this assignment you will just disable the “Save” button. To help the user, provide meaningful initial placeholder text in both input elements.
Recall that we use controlled components. Store the state of the values you are getting from the user and use those value and onChange
call backs to keep the input elements in sync with the state (see the ColorPicker example). You should not extract the values from the underlying DOM elements. You do not need to wrap our inputs tags in a <form>
tag. Doing so can create problems for saving the data.
Routing to the editor
Just as we are now using routing to switch between articles, we would like to use routing to switch to the editor. Specifically, http://localhost:3000/edit should bring up a blank editor, and http://localhost:3000/articles/42/edit should allow us to edit the article with id 42 (we will get to that shortly).
Create a new page in pages named edit.js
. Inside, create a new component named SimplepediaCreator
(the name is important, the tests are expecting it). Your component will be similar to SimplepediaArticle
(in pages/articles/[[...id]].js
), but will have collection
, setCollection
, and setCurrentArticle
props and render an Editor
component. The setCollection
prop has already been implemented in _app.js
and will update the collection when passed an array.
Update the collection
Your Editor component expects a complete callback. You will write it in SimplepediaCreator
. This callback should check if there is an article. If there is, add it to the collection (remember that you can’t just push it on the end of the array, you need to create a new array to trigger the rerendering). Before you add the new article to the collection, you will need to give it a unique id
. Scan through the collection to find the largest id
, and give your new article an id that is one more than that. Once the article is added, invoke the setCurrentArticle
callback to render the newly created article. If the article argument is missing (undefined), then the user canceled. Invoke router.back()
to return to the previous page.
Part 5: Updating IndexBar
We want to ensure that the IndexBar
section is consistent with the new “current article”, i.e. it is showing all the titles in the same section as the newly created article. Pass currentArticle
to IndexBar
as a prop (there are other ways to accomplish this goal, but this is the approach we will use in our assignment to minimize changes from assignment 2). Unfortunately, you can’t just use the first character of the title to determine the section, because then we couldn’t switch sections.
To get around this, we will introduce another state variable to IndexBar
: prevCurrentArticle
. This state variable will remember the value of the current article so we can tell when some outside component changes it. Here is the structure:
const [prevCurrentArticle, setPrevCurrentArticle] = useState(currentArticle);
if (currentArticle !== prevCurrentArticle) {
setPrevCurrentArticle(currentArticle);
// Code to update section state appropriately
}
::: {.callout-warning}
Wait, didn’t I say in class that you should not call state setting functions during the render of the component?
Yes, I did, and don’t make a habit it of it. However, in this instance, we won’t enter into a re-rendering loop because of how we are using it. When the currentArticle
doesn’t match the remembered prevCurrentArticle
, we know it is because the current article has changed externally and we can use it to change the current section. At the same time, we update prevCurrentArticle
to match currentArticle
. These two things will trigger a rerender, but the next time we are guaranteed we won’t go through this process again because the two state variables now match.
:::
It is possible for the user to create a new article with the same title as a current article. We will ignore that problem in this assignment as we will fix it in the next assignment when we introduce a server (which will validate that the title is unique).
Part 6: Allow editing of existing articles
Once you can successfully add new articles, you will adapt the interface to allow editing of articles. Add an “Edit” button to the ButtonBar
to request the current article be edited. When clicked, the edit button should call handleClick
with the string "edit"
. When handleClick
is invoked with “edit”, you should route to /articles/[id]/edit
, with the id of the article to be edited (e.g., /articles/42/edit
).
Create the page in the file pages/articles/[id]/edit.tsx
(i.e., a directory named [id]
inside the articles
directory containing a file named edit.tsx
). Since this route is more specific than the catch-all route we created earlier it will take precedence. This page will be similar to the edit.js
you created previously, but the component will be named SimplepediaEditor
and take an additional prop currentArticle
.
Pass currentArticle
to Editor
as the currentArticle
prop. Modify Editor
to initialize the values of the form fields with the current title and body. currentArticle
is not always defined to so when we initialize the state we will need to handle the two different cases, when currentArticle
is defined and when it is not. In the former, for example, we would want to initialize the title state with currentArticle.title
and in the latter, we would want to initialize the title state with ""
(the empty string).
There is an additional subtlety. Next may render SimplepediaEditor
before it has extracted the id
from the URL (and set currentArticle
). SimplepediaEditor
should effectively reset the Editor
component whenever the current article changes (i.e., each instance of Editor
is specific to a particular article). To do so, we specify the key prop to Editor as key={currentArticle?.id}
. This tells React that Editors
for two different existing articles are distinct and should not share state.
The optional chaining operator, ?.
, helps us concisely handle the situation where the component is rendered before currentArticle
is available. currentArticle?.id
is equivalent to
=== null || currentArticle === undefined) ? undefined : currentArticle.id (currentArticle
i.e., evaluates to undefined
is currentArticle
is undefined
instead of generating a “Cannot read properties of undefined” error.
On “Save”, the date should be updated and the changes saved (and the newly edited article displayed). On “Cancel”, the changes should be discarded leaving the article unmodified and the previous article view should be restored (displaying the original, unedited, article).
Part 7: Improve the UI
One principle in UX design is to not allow the user to perform actions when the actions don’t make sense. We have already done this in Editor
, where the “Save” button isn’t enabled unless the user has added a title. The user also shouldn’t be able to edit if there isn’t a current article. Add another prop to ButtonBar
called allowEdit
. When True, the “Edit” button should be visible. When False, it shouldn’t be visible. Note we are not just disabling the button, but actually not showing if there isn’t a current article.
Reflection
In the README.md file, I would like you to write a brief reflection about your assignment.
If it meets the requirements (passes all of the automated checks), write how comfortable with your solution. Did you use good development practices, or is it a hacky, last minute solution. Do you understand your own solution? How much effort did you put into it? Do you feel like you could do a similar assignment with more ease?
If it doesn’t yet meet the requirements, what are you struggling with? What does it not do correctly? What have you done to try to correct it? How hard have you tried to correct it? How much effort have you put into the entire assignment?
Put a date on the reflection and if you do any revisions, add another dated reflection below it.
Finishing up
Your submission should not have lint warnings or errors when run with npm run check
. Remember that you can fix many errors automatically with npm run check --write
. You should also make sure that your code passed all type checks by running pnpm run typecheck
.
Submit your assignment by pushing your changes to the GitHub repository. Once the repository is fully pushed, submit your repository to Gradescope as described here. You can submit (push to GitHub and submit to Gradescope) multiple times.
Portions of your assignment will undergo automated grading. Make sure to follow the specifications exactly, otherwise the tests will fail (even if your code generally works as intended). Use the provided test suite (run with pnpm test
) to get immediate feedback on whether your code follows the specification. Because of the increased complexity of a React application, Gradescope can take minutes to run all of the tests. Thus you will be more efficient testing locally and only submitting to Gradescope when you are confident your application meets the specification.
Labeling
For this assignment, I will label submissions with one of the following
Assessment | Requirements |
---|---|
Revision needed | Some but not all tests as passing. |
Meets Expectations | All tests pass, including linter analysis (without excessive deactivations). |
Exemplary | All requirements for Meets Expectations and your implementation is clear, concise, readily understood, and maintainable. |
Advice
You will go astray if you worry too much about the flow of interactions through your application. While information flow is important, focusing on it will have you attempting to code everything linearly. Practice compartmentalization and trusting that other components will work. You should also remember that as you write React components, the primary question should be “given the current state and props, what should I render?”
FAQ
Do I need to implement unit testing?
We will learn later in the semester how to unit test React components. For this assignment you are not expected to implement any of your own unit tests. The skeleton includes some unit tests to assist you in your development and to ensure that the grading scripts can automatically test your submission.
What if the tests and assignment specification appear to be in conflict?
Please post to Campuswire so that we can resolve any conflict or confusion ASAP.
I am getting an error about no router instance found. What is going on?
Error: No router instance found. you should only use “next/router” inside the client side of your app. https://nextjs.org/docs/messages/no-router-instance
The immediate cause of this error (despite what it reports) is trying to use the router, i.e., the value returned by useRouter
, before it is ready. For example invoking `router.push(…)`` before router is ready. It takes some time before the router is ready, specifically it is not ready for use when your components are first created.
The root cause of this error is typically invoking a handler function (that in turn performs operations on the router) during rendering as opposed to providing it as a callback. In the following example
<button onClick={complete()}>Cancel</button>
complete
is being invoked (with no arguments) during render, i.e., immediately, not passed as a callback to be invoked when the user clicks the button. Note the difference with the following correct approach where we are passing function to onClick that will invoke complete
<button onClick={() => complete()}>Cancel</button>