Assignment Four
October 23, 2025 at 11:59 PM
Goals
- Work on integrating Supabase into a more complex application
- Experience refactoring to fit changing requirements
Prerequisites
This assignment builds on the work of assignment 3. As such, you should not start it until you have passed all of the Gradescope tests for assignment 3. While this doesn’t assure you that you will have “met expectations”, it is enough to proceed.
Click the GitHub Classroom link and then clone the repository GitHub classroom creates to your local computer (in your shell, execute
git clonefollowed by the address of the repository).Update the
package.jsonfile with your name and e-mailInstall the package dependencies with
pnpm installCopy over your assignment 3 code into the starter project. Do this carefully as there are some changes you don’t want to overwrite.
Practice good software development habits and create a new feature branch for your work.
Once you have the dependencies installed you can start the development server with
pnpm run dev.
Background
This assignment mirrors the Supabase practical in which you converted FilmExplorer to use a database for data persistence. You will be transitioning Simplepedia over to server-maintained data management. You should complete the practical before starting this assignment.
I recommend that you keep the Supabase documentation and Supabase JavaScript API reference handy.
Data modeling
There are a couple of different ways that we could handle the data. We could do what we did for FilmExplorer and just dump all of the articles into an “articles” collection. At the start, we download them all and then if there is every an update we can write the change back to the database. This would require minimal work on your part since we would just be swapping out the loading the data from a JSON file with loading it from a database.
While this isn’t the worst idea, it isn’t a great one either. This approach won’t scale. As I mentioned in class, imagine if Wikipedia downloaded its entire library every time you visited the site. So we want ot think about our needs:
- we want to be able to see all of the sections all of the time
- we want to be able to see all of the titles that belong to a particular section
- we want to be able to see and edit the contents of an entire article, but just one at a time
- we want to be able to add and update articles, but reading will happen more often
- we want to minimize the number of reads and the amount of data that we are moving and storing
- we are also working under the rule that we can’t read only part of an article
With those constraints and desires in mind, here is how we are setting up the data:
- there is an “articles” table that holds all of the articles
- there is a “sections” view that holds a set of unique sections
- the “articles” table will hold full article objects (with autogenerated ids)
- the rows in “articles” will have fields
title,contentsandedited
This is not the only way we could have structured this data, but it is a pretty clean way that still meets our criteria.
What is a view?
When we make a query on a database, the results are essentially expressed as a table. A view is basically a named query that lives in he database that we can query against as if it were a table. The results are not physically stored – the system reruns the query as needed. This can provide us with an alternate slice of the data that appears to update in realtime as the tables that it is based upon change. Views can be used in queries as if they were tables, so they are a great way to simplify complex queries that are performed frequently.
For our purposes, we are using the view as a way to get around some of the limitations of the Supabase API. PostgreSQl has a fairly rich set of data manipulation tools included in their version of SQL, but these are not exposed through the Supabase interface. In particular, there are string manipulation functions we can use so that the database does all of the work of determining what the set of section headings is. From the perspective of our code, it will look like there is a sections table that always knows what the sections are.
It will be important to understand this structure. After the seeding step below make sure to take a few minutes familiarizing yourself with the layout by browsing the contents in the dashboard.
Setting up Supabase
You should follow the process of practical 05 fairly closely through this process.
- Run
pnpm supabase initto initialize the system. This will create a supabase directory in your project root. You will be asked if you want to setup settings for Deno for different environments. You can reply “n”. - Start the supabase server with
pnpm supabase start - Save the key values you need in a .env.local file
I have provided the src/lib/supabase_client.tsx source file again as there are no changes that need to be made to it form the practical.
Between this assignment, the practical and the project, you will have a collection of different supabase instances to manage. Probably the best way to manage this is to stop the current instance whenever you switch away from what you are working on with
pnpm supabase stopIf you find you really want to run two simultaneously, you can edit the port number in supabase/config.toml to an unused port number before you run pnpm supabase start.
Loading the data
Follow the process from the practical to create a migration.
Here is what you will put into that migration:
create table if not exists articles (
id bigint primary key generated always as identity,
title text,
contents text,
edited timestamptz default now()
);
create view sections as SELECT
distinct left(title, 1) as section
from public.articles;Points to note:
- we are using an autogenerated
idlike we did in FilmExplorer - we are using a different data type for
edited. We are actually storing the data as an date object. By default, it will be set to the moment when the associated record is created. - the view is querying
public.articles. Theleft()function takes a slice of the leftmost characters from the text. Thedistinctpart means that it will make sure that there are no duplications in the section listings once we strip the titles down to single letters.
To seed the database, copy over the seed.js file from the practical and make the appropriate changes.
Run pnpm db:reset to update the database and load all of the data.
Database functions
As with the practical, we are going to make a thin wrapper over the database calls. In src/lib/db_functions.ts you will find a collection of function stubs. This file is the only place where you should have any use of supabase outside of supabase_client.ts.
The functions are:
fetchSections()- returns an array of strings (sections)fetchTitles(section:string)- returns an array ofTitleTypeobjects corresponding to the providedsection.TitleTypeobjects only have fields foridandtitlefetchArticle(id: number)- returns the article object associated with the providedidornullif no matching article was foundupdateArticle(article:Article)- thearticleparameter represents the updated contents of the article. Returns theerrorif there is one.addArticle(article:Article)- adds the providedarticleto the database. Returns an object with two fields:id(the new id for the article) and `error (if there is one)deleteArticle(id:number)- deletes the article associated with theid. Returns the error if there is one.
You should start with these, then they will be available when you need them.
Refactoring IndexBar and its children
I suggest tackling this one piece at a time. For starters, you can remove the collection prop from IndexBar. You can then remove any of the code that used collection. Give SectionsView and TitlesView empty arrays until you have rea data for them.
Sections
Tackle the sections first. You don’t want to hit the database on every page load, so you will want a state variable (sections would be a good choice) and a useEffect to manage the fetching.
Some hints:
- recall that data fetching is asynchronous – you will need a helper function inside of the
useEffect - remember to fetch from the
sectionsview – most of the work is already done for you that way - there is a function in the Supabase API that will order your results so you don’t need to handle that either
- the results will be an Array of objects that look like
{section: "A"}. You will want to transform that to a straight list of letters. - in types/Simplepedia_types.tsx I have given you a
SectionTypetype. You can use this for the data returned by your query (before you transform it). - think about when you will need to refetch the sections
- in
SectionsView, you will no longer need to make a copy or sort.
Make sure that you can see the list of sections before yuo move on.
Titles
Handling the titles will be very similar to handling the sections. Again, you will want a state variable and a useEffect to handle the data fetch.
For the titles, you don’t need to fetch the whole article – just fetch the id and the title. I’ve provided a TitleType that you can use for this structure.
- you will want to look at the
ilikefunction - these results could also be sorted
- you will probably need to do some surgery to
TitlesViewto get the types to agree - make sure
TitlesViewis only sending theidtosetCurrentArticle - in
TitlesViewyou will no longer need to make a copy or sort
Make sure that you are displaying the right titles when the section is changed.
Once the sections and the titles are working, go into SimplepediaArticle and remove the collections prop from IndexBar.
Create a useCurrentArticle custom hook
One of the changes we are going to make for this assignment is that we are going to move all of the currentArticle logic into a custom hook. Custom hooks are basically slightly privileged functions that are allowed to use other hooks like useState and useEffect (functions we can’t make use of in a “normal” helper function).
We have provided you with the start of the hook in src/hooks/useCurrentArticle. The code includes a useState for storing the current article and it gets the id from the router. We also provided you with the type signature of the hook (UseCurrentArticleReturn), which tells you that this hook should return an object with two properties, currentArticle, which should be an Article or it should be null, and setCurrentArticle, which should take in either a number (id) or nothing.
Notice that the setter in the useState is not setCurrentArticle. The setter is just for loading the state. The setCurrentArticle we will return from this function should set the URL with the router.
Copy your setCurrentArticle function that uses the router to set the URL based on the id into the hook. Adjust the return statement so it returns an object with the currentArticle and setCurrentArticle properly set.
The last piece here is to actually get the current article from the database (when appropriate). You will need another useEffect that fires whenever the id changes. if the id is undefined or null, it should set the current article to null. otherwise it should fetch the appropriate article from the database.
Once the hook is complete, add it to SimplepediaArticle. You can remove the currentArticle and setCurrentArticle props. Make sure you can view articles.
After that is working, you can do the same thing in SimplepediaCreator and SimplepediaEditor. This is the beauty of writing this as a hook. Instead of writing all of this code into the top component of the application and then drilling down through the props, we can add this logic in wherever we actually need it.
It is worth noting that these three uses of the new hook are not sharing state, which is what we would have if we put it at the top. However, since these are three different operations, we should have no conflicts caused by this, though there may be an extra fetch or two since we aren’t caching the query results.
Updating the database
Once you are able to browse again using the remote collection, it is time to handle making changes. There are two places you need to make edits: SimplepediaEditor and SimplepediaCreator. In general the edit will remove more code than it adds.
In SimplepediaEditor, you need to handle updates. In Assignment 3 you needed to make a copy of the article and then copy the entire collection to merge in the change. Now, you can just use the update() method on supabase. The update method takes in an object to determine which fields to update and how to change them. That means we can just pass it our new article object almost as is. The only exception is that we need to remove the id field.
One of the simplest ways to remove a property from an object in JavaScript is to destructure and spread at the same time.
const {id, ...modifiedArticle} = article;When we use the spread operator inside of the destructuring syntax, it is saying “whatever is left over”. So this statement separates the article into the id and everything else, putting everything else into modifiedArticle.
The only problem with this approach is that we may then have an unused variable that the linter will complain about. This is an appropriate moment to use the override comment to ignore this error.
The story is much the same in SimplepediaCreator using the insert function. previously, we had to work through the whole collection to figure out what id to give our article. That is not out of our hands. Because of the schema we provided for the articles table, when we do an insert operation it will automatically assign a new id.
The wrinkle with letting the database handle creating the id is that we need it to set the article as the new currentArticle. You can address this by adding select('id') at the end of the query, after insert. This will do a followup selection just on the item you inserted and will return just the id value. use this to set the current article.
Cleaning up
With all of these changes, none of our pages really need props any more. Clean out all of the props you aren’t using any more. This will take you back to the MainApp component which is providing those props. Strip out everything about collection and currentArticle from MainApp. When you are done, the component will be much simpler and just provide the basic page layout.
Adding delete functionality
Your last task will be to add deletion to Simplepedia. Add a new button to ButtonBar that says “Delete”. it should only be visible when allowEdit is true. When it is clicked, it should send “delete” to handleClick
In SimplepediaArticle, add the new action to handleClick. use Supabase’s delete function to remove the current article and then route to “/articles/”.
Testing
In addition to all of the automated tests, you should test out the functionality using the dev server.
Some things to test:
- selecting a section changes the list of articles appropriately
- selecting a title shows the corresponding article
- selecting a section that an article doesn’t belong to clears the article
- after creating a new article, the article becomes the current article and the current section and corresponding titles update appropriately
- if the new article belongs to a section that previously had no articles, this still holds true
- after updating an article, it becomes the current article with the correct article titles showing – even if the title was changed, moving it to a new section
- deleting an article removes it from the view and removes the title
- deleting the last article from a section will clear the current section so we see the “Select a section” message
If some of these tests are failing, the first place to look is the useEffect functions in IndexBar. Look carefully at the watch lists and then think through what each one should do when a change occurs.
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.
If you followed the advice to work in a feature branch, merge your changes back into main.
Push 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. |
FAQ
Do I need to implement unit testing?
No. As with previous assignments, 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 Slack so that we can resolve any conflict or confusion ASAP.