Miniproject
Duration: Two weeks, weeks 41 and 43
Routing: React Router 7, declarative mode
Build a React single-page application for managing a personal book collection. Users must be able to add, view, edit, delete, and mark books as read or unread. Organise the application into pages using React Router, nested routes, and shared state through outlet context.
The navigation bar belongs in App and must remain visible on every page. Route changes replace the content below the navigation bar.
Use React state for the book collection, props for reusable components, and hooks for state, routing, and persistence. A backend is not required.
By completing the project, you should be able to:
- Define static, dynamic, index, and nested routes.
- Build shared layouts using
Outlet. - Navigate using
Link,NavLink, anduseNavigate. - Read route parameters with
useParams. - Share state and update functions through
Outlet contextanduseOutletContext. - Update state immutably and reuse a form through props.
- Persist state using
localStorageanduseEffect.
| URL | Component | Purpose |
|---|---|---|
/ | Redirect | Redirect to /books. |
/books | BooksPage | Display all books. |
/books/create | CreateBookPage | Create a book. |
/books/:id | BookDetailsPage | Display one book and its read status. |
/books/:id/edit | EditBookPage | Edit the selected book. |
| Any unmatched URL | NotFoundPage | Show a page-not-found message. |
Use /books/:id/edit rather than /books/edit: the ID identifies which book to edit. For example, /books/b1/edit edits the book whose ID is b1.
Create a React project with Vite, or extend your existing project. Examples below use JavaScript; TypeScript is also acceptable.
Install React Router 7:
npm install react-router@7
Import routing components and hooks from react-router.
Represent each book with these fields:
{
id: "b1",
title: "The Hobbit",
author: "J. R. R. Tolkien",
isRead: false
}
Use string IDs throughout the application. Generate a unique ID when creating a book, for example with crypto.randomUUID(). Keep the same ID when editing. Do not use array indexes as IDs or React keys.
Start with two or three sample books.
Create placeholder page components first so you can verify routing before implementing the book features.
Use this route structure in main.jsx. Add imports for your own components and retain your project’s CSS import.
import { createRoot } from "react-dom/client";
import { BrowserRouter, Navigate, Route, Routes } from "react-router";
// Import App, BooksLayout, and all page components here.
createRoot(document.getElementById("root")).render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Navigate to="/books" replace />} />
<Route path="books" element={<BooksLayout />}>
<Route index element={<BooksPage />} />
<Route path="create" element={<CreateBookPage />} />
<Route path=":id" element={<BookDetailsPage />} />
<Route path=":id/edit" element={<EditBookPage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
);
Child paths are relative to their parent. The index route under books renders at /books; it does not add a URL segment.
Checkpoint: Every required URL displays its placeholder page, and / redirects to /books.
Make App the shared layout and the owner of the book collection. Render a navigation bar above its outlet.
Your App should have this structure:
import { NavLink, Outlet } from "react-router";
export default function App() {
// Create books state here.
// Implement addBook, updateBook, deleteBook, and toggleRead here.
return (
<>
<header>
<nav aria-label="Main navigation">
<NavLink to="/books" end>Books</NavLink>
<NavLink to="/books/create">Add book</NavLink>
</nav>
</header>
<main>
<Outlet
context={{ books, addBook, updateBook, deleteBook, toggleRead }}
/>
</main>
</>
);
}
This is scaffolding: implement the state and functions before using it.
Style the active navigation link. The end prop makes the Books link active only at /books.
Use Link or NavLink for internal page navigation. Use buttons for actions such as deleting a book or toggling its read status.
Checkpoint: The navigation bar remains visible on the list, detail, create, edit, and not-found pages. Navigating between pages preserves the book collection.
Create a BooksLayout that adds a shared heading and renders the selected book page:
import { Outlet, useOutletContext } from "react-router";
export default function BooksLayout() {
const bookContext = useOutletContext();
return (
<section>
<h1>My book collection</h1>
<Outlet context={bookContext} />
</section>
);
}
BooksLayout receives the context from App’s outlet and forwards it through its own outlet. Keep this forwarding: the book pages read the context provided by their nearest parent outlet.
In a page component, access only the values you need:
const { books, deleteBook, toggleRead } = useOutletContext();
Implement these functions in App:
| Function | Required behaviour |
|---|---|
addBook({ title, author }) | Add a book with a unique ID and isRead: false; return its ID. |
updateBook(id, { title, author }) | Update title and author while preserving ID and read status. |
deleteBook(id) | Remove the book with this ID. |
toggleRead(id) | Toggle the selected book’s isRead value. |
Use functional state updates when deriving the next state from the previous state. Use map, filter, and object/array spreading instead of mutating the existing collection.
Use outlet context in routed page components. Pass ordinary props to reusable components such as BookList, BookItem, and BookForm.
Checkpoint: Explain the data flow from App through BooksLayout to a page, and from the page to a reusable component.
In BooksPage:
- Read the books and relevant update functions through
useOutletContext. - Render a separate
BookListcomponent and pass data and callbacks as props. - Display each book’s title, author, and read/unread status.
- Link each title to
/books/:id. - Add an Edit link to
/books/:id/edit. - Add buttons to delete a book and toggle its read status.
- Show a useful empty-state message when there are no books.
Example links inside a BookItem:
<Link to={`/books/${book.id}`}>{book.title}</Link>
<Link to={`/books/${book.id}/edit`}>Edit</Link>
Checkpoint: Deleting or toggling a book updates the list immediately.
In BookDetailsPage:
- Read
idusinguseParams. - Read the collection using
useOutletContext. - Find the matching book.
- Display its title, author, and read status.
- Provide an Edit link, a read/unread button, and a Back to books link.
Start with:
const { id } = useParams();
const { books } = useOutletContext();
const book = books.find((book) => book.id === id);
If the book does not exist, render a “Book not found” message and a link to /books. Do not access its properties before checking that it exists.
An unknown book ID is different from an unmatched route: /books/missing-id matches the details route, but its book may not exist.
Checkpoint: A valid ID displays the correct book; an unknown ID shows a helpful message without crashing.
Create a reusable BookForm with controlled inputs for title and author. Its props should include initial values, a submit callback, and a cancel callback.
In CreateBookPage:
- Read
addBookfrom outlet context. - Render
BookFormwith empty initial values. - Reject empty or whitespace-only title/author values and show feedback.
- Prevent the browser’s default form submission.
- On successful submission, add the book and navigate to its detail page.
- On Cancel, navigate to
/bookswithout creating a book.
Use useNavigate after a successful action:
const navigate = useNavigate();
function handleCreate(values) {
const id = addBook(values);
navigate(`/books/${id}`);
}
Checkpoint: Creating a book opens its details, and it appears in the list.
In EditBookPage:
- Read the ID from the URL.
- Find the book through outlet context.
- Handle a missing book before rendering the form.
- Reuse
BookForm, prefilled with the existing title and author. - On Save, call
updateBookand navigate to/books/:id. - On Cancel, return to the details page without changing the collection.
Keep unsaved input values inside BookForm. Do not update the shared collection on every keystroke.
If the form initialises local state from its initial values, make sure that editing a different book loads that book’s values. One option is to render the form with key={book.id}.
Checkpoint: Save changes the book’s title and author while retaining its ID and read status. Cancel leaves the book unchanged.
Persist the collection in localStorage:
- Initialise App’s state from stored books using a lazy state initialiser.
- Use sample books when no stored collection exists.
- Preserve a saved empty array: deleting all books must not restore the samples after refresh.
- Use
useEffectto save the collection whenever it changes. - Handle invalid stored JSON without crashing.
Do not use an effect to find a book by ID; calculate that from the current collection during rendering.
Implement NotFoundPage with a link back to /books.
Checkpoint: Create a book, refresh its detail URL, and verify that the book still exists. Delete every book and refresh to verify that the collection stays empty.
| Week | Deliverable |
|---|---|
| 41 | Nested route setup, permanent navigation, outlet context, list and detail pages, delete and read/unread actions. |
| 43 | Reusable create/edit form, validation, persistence, error handling, styling, verification, and README. |
- All required routes work.
- App’s navigation remains visible on every page.
- App and BooksLayout both render an outlet.
- BooksLayout forwards outlet context to its child pages.
- The collection has one source of truth in App.
- Routed pages use
useOutletContext; reusable components use props. - Users can add, view, edit, delete, and toggle books.
- Creating and saving an edit navigate to the correct details page.
- Cancel does not change the collection.
- Unknown URLs and missing books show helpful messages.
- Books survive refresh, including an empty collection.
- Browser Back and Forward work as expected.
- Inputs have labels, validation feedback is visible, and the layout works on mobile and desktop.
- Search by title or author and filter by read status.
- Store search/filter settings in URL query parameters using
useSearchParams. - Add meaningful interaction tests for creation, editing, and missing books.
- Add a backend API and loading/error feedback.
If you deploy the application, configure the host to serve the SPA entry page for client routes so directly opening or refreshing /books/:id works.