Programming Assignment 3
Initial Due Date: 2024-10-03 11:59PM Final Due Date: 2024-10-25 4:00PMGithub Classroom Gradescope
In this assignment, you will continue your implementation of Simplepedia.
Learning Goals:
- Practice implementing React components, including incorporating PropTypes, by adding editing functionality (to create or update articles)
- Advance your understanding of the division of responsibilities and state between components
- Practice "agile" development when requirements change over time
- Commit and push your changes to GitHub
- Submit your repository to the Gradescope assignment
Prerequisites
This assignment builds on the work of assignment 2. As such, you should not start it until you have passed all (or at least most) 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
đ» npm install
Once you have the dependencies installed you can start the development server with đ» npm 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 đ» npm 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 your astray. While information flow is important, focusing on it will have you tied in knots try 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
Update the Assignment 3 skeleton with the code you wrote for assignment 2. Be a little cautious as you do this, there are some changes and you donât want to just replace the files with your old ones. Some specific changes to keep an eye out for:
- The article collection is now a prop of
Simplepedia
instead of being state. If you take a look in_app.js
you will find the dataâs new home. - 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 for the old behavior.
Part 1: Add PropTypes
Please add PropTypes to your existing modules. You will find that ESLint is now checking for PropTypes, so your code will no longer pass the linter without them.
When adding PropTypes, be as specific as possible. There are a lot of different validators. Note that these cover specifying the contents of arrays and the structure of objects. For example, for the Article
component, donât just specify that it receives an object prop, specify what fields are required in that object. If you have an Array
prop, specify what type should be in the array.
As a starting point Iâve provided ArticleShape.js
in components (and an example of its use in index.js
for the Simplepedia
component). This is a full description of the article type, and you can use it in your PropType descriptors. After you implemented your PropTypes, make sure that eslint and jest agree that you have added PropTypes for everything.
Part 2: Switch to routing (requirements change!)
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 this section first and check out relevant FAQ entries below).
After this 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 how we could make a new .js files in pages
and they would become âpagesâ in our single-page application. We are not going to make a new page for every single article. Instead, we are going to use a facility in Next called dynamic routes. Dynamic routes are essentially matchers. They allow us to introduce variables into the route that are available to our code.
We specify a dynamic route by putting the name in square brackets. We are going to use different forms of dynamic routing, including a special form called an âoptional catch all dynamic routeâ. Donât get sidetracked by all the possible features of Next.js, for this assignment, please focus on implementing the following steps:
- 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.
The id
in the file name is actually 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 Next router state. In anticipation of adding a second page, we are going to extract the article id in _app.js
.
Within _app.js
:
- Import
useRouter
withimport { useRouter } from "next/router";
- Add
const router = useRouter();
to the top of the component - Access the
id
variable withconst { id } = router.query;
(note the destructuring assignment) in the body of theMainApp
function.
Try console logging the id
and navigating to http://localhost:3000/articles/42 in your browser to make sure you can extract the id from the URL. 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
you obtained from the router to look up the article in the collection. The find
function would be a good choice. Note that:
- 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.
Similar create a function setCurrentArticle(article)
inside the MainApp
component to use as the setter. This function will be responsible for programmatically changing the URL. 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")
.
Why push? 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.
Then add currentArticle
and setCurrentArticle
to the props created in MainApp
and to the props expected by the Simplepedia
component ([[...id]].js
). At this point you should be able to remove the current article state you previously implemented in Simplepedia
(i.e., 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.
At this point you should be able to visit individual articles by URL, e.g. http://localhost:3000/articles/42!
Remap application entry point
If you try to navigate to http://localhost:3000, you will get a 404 error (âThis page could not be foundâ), since we moved the index.js
file that was loaded for that URL. In our new version, /articles
is really the starting point of the application. We will utilize Nextâs redirect feature to redirect all requests to the root page /
to /articles
. Create a file in the root directory of your application named next.config.js
with the following contents. Note that you will need to restart the development server for this change to take effect. When you do so, entering http://localhost:3000 should automatically redirect you to http://localhost:3000/articles.
module.exports = {
async redirects() {
return [
{
source: "/",
destination: "/articles",
permanent: false,
},
];
},
};
Windows Warning
Certain versions of Prettier have a known issue on Windows when running as part of a commit hook on the Next.js dynamic routing files, e.g., [[âŠid]].js. Unfortunately, it looks there is no work around at the moment, instead we are waiting for updates in upstream packages to percolate to Prettier. While I believe that is now the case, if you still observe this problem, create a commit with just those files using the --no-verify flag
with the commit command, e.g., git commit --no-verify -m "Files that Prettier is having problems with"
, to bypass those checks.
Part 3: Allow the user to create new articles
You will find a new Editor
component in src/components/Editor.js
. 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). In this part you should pass a single prop to the Editor
component: a ârequiredâ callback named complete
that takes an article object as its argument and creates the article (note the skeleton implements a second prop that we will use later). Check out the FAQ entry about article objects.
Make sure to use these names (and any others specified in the 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 (your editor styling does not need to match the example above). The first should be a âSaveâ button. When the user clicks the âSaveâ button, the article should be added to the collection, and you should automatically navigate ot the newly created article. The date on the article should be set to the current date and time (as an ISOString via the toISOString()
method on Date
objects). The second should be a âCancelâ button. When the user clicks cancel, the new article 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 Simplepedia
(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 4: Add some buttons
The Simplepedia
component will be responsible for switching between adding and viewing. Implement the component named ButtonBar
in src/components
. It should display a single button called âAddâ. The component will take a single callback prop named handleClick
. When the button is clicked, the component should call the function, passing âaddâ as an argument (i.e., handleClick("add")
). Add this new component to Simplepedia underneath the Article component.
In Simplepedia
write the handleClick
callback so that when it receives the command âaddâ, it uses the router to take the user to the editor.
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. Instead, we will use an effect hook to update the section state whenever (and only when) the currentArticle
prop changes. We will discuss useEffect
in class shortly.
The basic concept is that we register a function to be called under certain circumstances (when state or prop values change). The useEffect()
function takes two arguments, the first is the function to run and the second is the list of variables to watch for changes. You will want to watch for changes to currentArticle
, changing the section if appropriate. This will mean that the section will change when the article changes, but can then be also changed independently when the user selects a new section.
useEffect(() => {
// Code to update section state appropriately
}, [currentArticle]);
React Note
useEffect
is not necessarily the ideal approach to implementing this interaction, but we suggest it here to minimize complexity. Either approach (the one described above, or the one described in this note) will be accepted. As described in the React documentation the problem is that the component will render, then the effect will execute, then the component may re-render again. To reduce the re-renders, the recommend approach is set the state directly during rendering whenever the currentArticle
changes. To do so, we need to figure out manually when currentArticle
has changed. We can do by storing the currentArticle
from previous renders, e.g.,
const [prevCurrentArticle, setPrevCurrentArticle] = useState(currentArticle);
if (currentArticle !== prevCurrentArticle) {
setPrevCurrentArticle(currentArticle);
// Code to update section state appropriately
}
Another subtlety is that the effect will always fire once when the component is first mounted. But that is not the case in our approach above. To ensure the section is set properly when the component is first mounted, initialize the section
state from currentArticle
if it defined when the component is first created (mounted). The argument to the useState
hook is only used when the component is first created and so can be used to initialize state for the first render.
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.js
(i.e., a directory named [id]
inside the articles
directory containing a file named edit.js
). 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 create 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 SimpleEditor
before it has extracted the id
from the URL (and set currentArticle
). SimpleEditor
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.
Javascript Note
The optional chaining operator, ?.
, helps us concisely handle the situation where the component is rendered before currentArticle
is available. currentArticle?.id
is equivalent to
(currentArticle === null || currentArticle === undefined) ? undefined : currentArticle.id
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 UX
One principle in user experience (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
named 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.
Finishing up
Your submission should not have ESLint warnings or errors when run with đ» npm run lint
. Remember than you can fix many errors automatically with đ» npm run lint -- --fix
(although since ESLint can sometimes introduce errors during this process, we suggest committing your code before running âfixâ so you can rollback any changes). The assignment skeleton includes the Prettier package and associated hooks to automatically reformat your code to a consistent standard when you commit. Thus do not be surprised if your code looks slightly different after a commit.
Submit your assignment by pushing all of your committed changes to the GitHub classroom via đ» git push --all origin
and then submitting 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 đ» npm 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.
Grading
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. |
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 Ed so that we can resolve any conflict or confusion ASAP.
What do you mean by creating a new article object?
When part 3 talks about creating new article objects, it means a new object in the Javascript sense, e.g., something like the following:
{
"title": "Alpha Centauri",
"contents": "An alien diplomat with an enormous egg shaped head",
"edited": "2017-05-08",
}
or what is sometimes called a âplain old javascript objectâ. It does not mean a âcapital Aâ article as defined in src/components/Article.js. The latter is the Article
React component and is only used for rendering articles, not for maintaining the article related data.
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>