Initial Due Date: 2025-03-13 9:45AM
Final Due Date: 2025-04-03 4:15PM
This assignment builds on the work of assignment 3. As such, you should not start it until you have passed many/most of the tests for assignment 3.
💻 git clone
followed by the address of the repository).package.json
file with your name and e-mail💻 pnpm install
Once you have the dependencies installed you can start the development server with 💻 pnpm run dev
.
This assignment builds on the the REST practical in which you incorporated an API into the color picker. You will be transitioning Simplepedia over to server-maintained data management.
You will be communicating with a server integrated into your application with NextJS API routes. The server is already implemented for you. Check out the files in the src/pages/api directory. (Note that this may not be the way you would implement this type of site with Next.JS. In subsequent classes we will talk about other methods for implementing server functionality within Next.JS.)
The server provides the following API (:id
indicates a parameter that should be replaced with a valid article id, while :section
indicates a parameter that should be replaced with a valid section):
Endpoint | Method | Action |
---|---|---|
/api/sections | GET | Fetch a JSON array of all sections in alphabetical order |
/api/articles | GET | Fetch the entire article collection as an array |
/api/articles?section=:section | GET | Fetch all articles in the corresponding section |
/api/articles?section=:section&titlesOnly | GET | Fetch all articles (but id and title fields only) in the corresponding section |
/api/articles | POST | Add a new article to the collection (the new article should be provided as the JSON-encoded request body) |
/api/articles/:id | GET | Get article with id of :id |
/api/articles/:id | PUT | Update the article with id of :id (entire updated article, including id should be provided as the JSON-encoded request body) |
In the case of both PUT and POST, the server will send back the new or updated article.
The server implements some server-side validations and will respond with an error (HTTP status code 4** or 5**) if your request or data is ill-formed or otherwise invalid. An example of the former is a missing or mismatched id
field in the PUT request body (it should match the URL). An example of the latter is creating an article with a duplicate title. I suggest keeping the browser developer tools open while working on the assignment to monitor your requests and the corresponding responses. Your application should handle errors “gracefully”, i.e. “catch” any errors that arise in your promise chain and not make any state updates if a fetch returns an error code (which manifests as a rejected Promise).
The server is using a local database, implemented with SQLite, for storing the articles during both development and testing. As a result the articles should persist during development.
To enable end-to-end testing, we are running the embedded API server during testing. You will notice your package.json file has a slightly different test command (which builds the production version of your application then starts the server and runs the tests). This testing approach may not be appropriate for other applications, but here it enables us to test our front-end application without needing to mock fetch
(like we did in the practical) or create a fake server.
This approach does introduce some limitations:
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.
Update the Assignment 4 skeleton with the code you wrote for Assignment 3, i.e. copy your pages and components to the same files in PA4. 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 notes:
collection
has been removed from the main component (_app.js) and its children in anticipation of fetching the data from the server. Note that due to this changes not all of our previous code will work until more of the refactoring is completeTo prepare the database for use, execute the following. You can recreate the database at any time by re-running these commands.
💻 pnpm exec knex migrate:latest
💻 pnpm exec knex seed:run
IndexBar
I suggest tackling this one piece at a time. So, start with displaying the correct sections with database fetched data. Then turn to the titles. Note that the tests assume that thefetch
es will occur in IndexBar
, not in its child components.
Instead of obtaining the sections from the articles in the collection, we will obtain them from the server (that way we don’t need to have all the articles available locally). Since the sections are no longer derived from the collection we will need to create a piece of state, e.g, sections
, to store that information. Start by creating the sections
state (initialized with an empty array) and then implementing a useEffect
hook that will populate that state with data fetched from the server. Since we only want this hook to execute once, when component first mounts, set the dependencies to be the empty array. Your newly created state can then be passed as the sections
prop to the SectionsView
component.
We similarly refactor the input to the TitlesView
component. Instead of filtering articles from collection
, we will obtain only the ids and titles of articles that belong to the relevant section from the server. That is, for efficiency reasons we only want to fetch the information we need, the article ids and titles. Notice that the API query parameters described above enable you to efficiently fetch just the data you need. As with the sections, we will need to create a new piece of state, e.g., titles
, to store these articles. Start by creating the state, and then implementing another useEffect
hook that will fetch the data from the server. Pay close attention to the dependencies of your hook. When will you want to perform a new fetch operation?
When we use a variable as a dependency of an effect hook, it will trigger that hook whenever the value changes. That is not always the same as when we want to perform the action in the effect, e.g., fetch new data from the server. That is, it is quite common to have additional conditionals inside the effect to limit when we perform the actions.
For example, if we have an effect with a boolean dependency shouldUpdate
, it will “fire” when that variable transitions from false to true, but also when it transitions true to false. But as its name suggests, we only want to do the action when shouldUpdate
is true. Thus we include a conditional within the effect.
useEffect(() => {
// Only do action when shouldUpdate is true
if (shouldUpdate) {
// Do action
}
}, [shouldUpdate]); // Fire effect when shouldUpdate changes
With this refactoring complete, you should be able to display sections and their corresponding titles using only data obtained from the server. Note that because TitlesView
will be receiving “incomplete” articles, you will likely need to refactor is PropTypes to allow for that. Instead of an array of full ArticleShape
s, it will now receive an array of the following (i.e., objects with just id
and title
properties).
PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
})
MainApp
When we had all the data available, we could make currentArticle
a variable, as opposed to a piece of state. Now we will need state to maintain that information over time. Replace your currentArticle
variable with currentArticle
state (you will likely need to rename your existing setCurrentArticle
callback function to something else to avoid naming conflicts with the state setter). With that state in place, create a useEffect
hook that will set the currentArticle
state based on the current value for id
(as extracted from the query
object in the router). When id
is not defined, we want to clear the current article. When id
is defined we want to fetch the corresponding article (and that article alone) from the server.
To try to reduce the number of article requests, we will only fetch an article when it is not already available locally. That is we want to fetch the article when id
is defined, and either currentArticle
is not defined or id
from the router (from the URL) doesn’t match currentArticle.id
(i.e., we have picked a different article). This implies that currentArticle
will need to be dependency of your useEffect
hook. To ensure that we always get the most up-to-date article, whenever we are switching articles we want to clear the currentArticle
so that we fetch the latest version of the article from the server (e.g., if we just created or edited the article). Note it would be even more efficient to use a complete article if available locally, but for simplicity we will not try to implement that optimization as part of this assignment (i.e., we will just re-fetch any newly created or modified articles).
Recall that depending on the route router.query.id
might be a string containing a single integer, an array of one element or undefined. To most effectively utilize useEffect
’s dependency tracking, we want to normalize the the value of id
outside of the useEffect
hook, i.e., in the component function body, so that when id
switches from "42"
to ["42"]
(or vice versa), we don’t trigger updates. That is we will use the now normalized integer as the hook dependency. Observe that the unary +
operator will convert strings and arrays of strings to integers, e.g.,
> +"42"
42
> +["42"]
42
If your application seems to work as expected, but you are failing a large number of tests, check that the names of your components and props align with those used in the automated tests and in previous assignments. A common issue is with the setCurrentArticle
prop needed by a number of your components. You might find yourself with a differently named function in that role. Don’t change the name expected by your children components, instead assign your new function to the setCurrentArticle
property in props
in _app.js (passed ot the rendered child components). For example,
const props = {
...pageProps,
setCurrentArticle: myFunction,
};
You should never modify the tests to make them pass, as the original tests will run on Gradescope!
SimplepediaCreator
and SimplepediaEditor
These two components differ in their implementation of their callback methods. Update the callbacks to create or update an article by making the appropriate requests to the server (POST to “/articles” to create a new article, PUT to “/articles/:id” to update an article).
fetch
takes a second argument that specializes the behavior of fetch
. With that argument we will want to specify the method, the body, and any headers. For example the following specifies a POST request, with JSON encoded data that is expecting a JSON response. Note that we need to manually encode the JavaScript objects as JSON with JSON.stringify
.
fetch(`/url`, {
method: "POST",
body: JSON.stringify(data),
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
});
The server is responsible for assigning a unique id
to each article (that is the only way to ensure the ids are unique). The POST request will send back the newly created article with its id.
You do not need to integrate the returned article into the titles or other state. Instead when the Simplepedia
component and its children are remounted after editing, they will fetch updated sections, the titles in that section, etc. Similar to the note above, we could imagine several possible optimizations to reduce the the amount of data fetched. For simplicity, we will not try to implement those optimizations as part of this assignment.
Unlike previous assignments, the server enforces constraints on the articles, specifically, that the titles must be unique. It will rejects POST and PUT requests with a duplicate title. You are not expected to handle these errors. Those constraints may impact your development approach, e.g., you can’t try adding an article with the title “Test” twice.
Recall that with the Next.js router, we can navigate directly to the editing page for an article, e.g., http://localhost:3000/articles/42/edit. If you notice that your editor remains blank and/or your are failing some editor-related tests, you might have an issue with initialization. Recall that it takes time to fetch the current article from the server. Thus your Editor
component may mount before the article is available and thus your fields are initialized with empty strings (and not reinitialized). Review the approach used in assignment 3 for handling this situation.
Your submission should not have ESLint warnings or errors when run with 💻 pnpm run lint
. Remember than you can fix many errors automatically with 💻 pnpm 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 💻 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.
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. |
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.
Please post to Ed so that we can resolve any conflict or confusion ASAP.
The current structure of Simplepedia is reflects the multi-assignment development process, the desire to minimize the use of NextJS-specific features and external libraries, and the many prior generations of these assignments. If you were to start from scratch, and exclusively target NextJS, you might take more advantage of NextJS’s features such as prefetching Links for the article titles and server-side (pre-)rendering (SSR) for articles. You might also use libraries like SWR that integrate the state and effect parts of client-side data access, support automatic re-validation and optimistic updates from local data.