Welcome back! 🎉 You already have a real UI foundation in place. Global styling works, the dashboard routes are set up, navigation exists, and both / and /tasks fetch live task data from the backend.
In this lesson, you’ll make the UI feel more like a real product by reducing duplication and introducing reusable building blocks. Instead of repeating the same markup and styles in multiple places, you’ll start extracting shared pieces into components that can be reused across the app.
There are three main improvements in this lesson. First, you’ll extract the repeated “task row” markup into a dedicated TaskRow component so task lists stay consistent. Next, you’ll introduce a reusable Button component so actions share the same look and behavior. Finally, you’ll evolve the layout into a sidebar-based dashboard shell that highlights the active route, which makes the app feel more structured and easier to navigate.
This keeps the codebase cleaner now, but it also matters later. As the app grows, reusable components make new features faster to build because you can extend existing UI patterns instead of rebuilding them from scratch.
In the previous lesson, you enabled Tailwind styling globally, built a dashboard route group with a shared layout and navigation, and fetched tasks from GET /api/tasks while rendering loading, error, and empty states.
Now you’ll take that working UI and organize it into reusable pieces. This is an important step in frontend development: a page can be “working,” but still need cleanup before it becomes easy to maintain.
There is still one key rule that matters throughout this lesson. Don’t modify anything under src/app/api/**. The backend is complete, and this lesson is frontend-only, so your job is to build better UI on top of the existing API rather than changing the API to make the UI easier.
Your UI consumes existing endpoints like: GET /api/tasks → { data: Task[], meta: { timestamp } }
That response shape is why you always parse tasks from json.data. The backend has already defined the contract, and the frontend should follow that contract consistently. In real projects, this kind of discipline matters because the frontend and backend are often developed separately, and changing one side casually can break the other.
A strong Codex prompt for this unit should always keep the scope tight and the constraints explicit. It should restrict edits to a small list of frontend files, and it should clearly include the rule “do not touch src/app/api/**.” That way, Codex is guided toward the exact work you want instead of making broad changes that solve the wrong problem.
When you render tasks in multiple places, like the dashboard’s “Recent Tasks” section and the full tasks list page, repeating the same markup becomes a maintenance problem. If you later want to change the badge color, adjust spacing, or update how due dates appear, you would have to make the same edit in multiple files. That kind of duplication makes small UI changes slower and increases the chance that one page ends up inconsistent with another.
So instead of repeating the markup, you create one source of truth: TaskRow. This is a very common React pattern. Pages stay responsible for page-level concerns like fetching data and deciding what list to render, while smaller components handle the repeated visual structure.
This code lives in src/components/tasks/TaskRow.tsx. It defines a reusable React component that takes a task object and renders the task title, a conditional status badge, and a due-date fallback. Once this component exists, every list in the UI can render tasks the same way with <TaskRow task={t} />, which makes the app more consistent and easier to update later.
-
This is a React component, which means it is just a TypeScript function that returns JSX. Components are the main way React lets you break UI into small, focused pieces. As your app grows, this becomes one of the most important habits to build, because it keeps page files from turning into long walls of repeated markup.
-
Taskcomes from@/lib/tasks, which helps the frontend stay aligned with the backend’s data shape. Instead of guessing which fields exist, the component works against a shared type that describes the task clearly. That makes the code safer and makes refactoring easier, because TypeScript can help you catch mistakes.
This code lives in src/app/(dashboard)/page.tsx. The dashboard already fetches tasks and computes stats, so the main change here is not about data logic. It is about UI organization.
Instead of keeping repeated task markup directly inside recent.map(...), you replace that duplicated structure with <TaskRow task={t} />. That makes the dashboard page easier to read because it focuses on page-level responsibilities like data fetching, state handling, and choosing which tasks count as “recent.”
This code lives in src/app/(dashboard)/tasks/page.tsx. The page still fetches tasks from the backend, but now each task is rendered with the shared TaskRow component instead of inline JSX.
That means both the dashboard and the tasks page now use the same visual building block. This is exactly the kind of reuse that makes a UI feel cohesive: the user sees the same task representation across different screens, and you only have one component to improve later.
-
This page is still a client component because it uses hooks like
useStateanduseEffect. In Next.js App Router, hooks only work in client components, so the directive is what makes this kind of stateful fetching possible. That distinction becomes more important as your app starts mixing server and client components intentionally.
Once your UI has navigation and lists, actions become the next thing users notice. Buttons are one of the most repeated elements in almost every app, so they are a great candidate for reuse.
If every page invents its own button styles, the interface starts to feel inconsistent very quickly. Some buttons end up with different spacing, different colors, or different focus behavior, even though they are supposed to represent the same kind of action. Creating a shared Button component solves that by giving the app one standard way to render actions.
This code lives in src/components/ui/Button.tsx. It creates a reusable button with primary and secondary variants, accessible focus styles, and disabled styling. That means pages can choose what kind of action a button represents without rewriting the base styling every time.
-
This is a reusable UI component built around a
variantprop, which is a very common design-system pattern. Instead of thinking in terms of “a blue button on this page” and “a gray button on that page,” you define a small set of button types with clear meaning. That makes the UI easier to scale because pages choose from established patterns rather than inventing new ones. -
ButtonHTMLAttributes<HTMLButtonElement>allows the component to accept standard button props likeonClick, , and . This is important because a shared component should not take away normal HTML behavior. A good abstraction keeps the useful native API while adding consistency on top.
Buttons should not only look consistent; they should also support meaningful actions that move the user through the app. In this lesson, you use buttons to navigate to /tasks/{id} and /tasks/new.
Even though those pages are still simple stubs for now, adding working navigation matters. It makes the app feel more complete, and it establishes the routes that future CRUD features will build on. In other words, you are shaping the user flow before all the final features are implemented.
This code lives in src/components/tasks/TaskRow.tsx. We add a “View” button that navigates programmatically with Next.js router support. This is different from Link: Link is ideal when the destination is part of the rendered markup, while router.push() is especially useful when navigation happens in response to a button click or other event handler.
-
useRouter()gives you imperative navigation, which means navigation triggered by an action rather than by static markup. This is a useful pattern for buttons like “View,” “Edit,” “Delete then redirect,” or “Save and continue.” It is especially common in forms and row-level actions, where navigation happens because the user performed some operation.
This code lives in src/app/(dashboard)/tasks/page.tsx. Here, you add a “New Task” button that navigates to /tasks/new with router.push(). This is another example of action-driven navigation rather than static link navigation.
Adding this button makes the tasks page feel more realistic. Instead of being just a place where data is displayed, it becomes a place where the user can start doing something meaningful in the app.
We want /tasks/new and /tasks/[id] to exist so the new navigation buttons do not lead to 404 pages. These pages are intentionally simple for now.
That simplicity is not a shortcut; it is part of good incremental development. You are defining the route structure first, proving that navigation works, and postponing the full CRUD implementation until the lesson that focuses on it. This helps learners build the app in manageable steps instead of trying to solve routing, layout, forms, and data updates all at once.
This code lives in src/app/(dashboard)/tasks/new/page.tsx. It gives the route a heading, a short explanation, and a link back to the tasks list so the page still feels intentional and usable.
-
This is a server component by default because it does not use hooks or client-only APIs. In Next.js App Router, that means the framework can render it efficiently without shipping unnecessary client-side JavaScript. Even a very simple page can reflect an important architectural choice.
-
Linkis enough here because the navigation is just part of the page markup. This makes the stub page feel connected to the rest of the app rather than like a dead end. Even temporary pages should support basic movement through the interface. -
The message on the page sets expectations clearly for the learner. The route exists now, the navigation works now, but the actual create form comes later. That clarity helps avoid confusion, especially in project-based learning where not every feature is built in the same lesson.
This code lives in src/app/(dashboard)/tasks/[id]/page.tsx. It demonstrates how dynamic routing works by reading the id route parameter and displaying it.
This is a small page, but it introduces an important concept. Dynamic route segments are one of the main ways apps model detail pages, edit pages, profiles, product pages, and other URL patterns where one template is reused for many entities.
-
[id]in the folder name creates a dynamic route, so a URL like/tasks/123maps to this page and provides{ params: { id: "123" } }. This is one of the core routing ideas in Next.js App Router. You will use it repeatedly whenever the app needs a page that depends on a specific resource ID. -
Displaying
params.idis a simple but useful way to verify that the routing is wired correctly. Before you build a full detail view, it is smart to confirm that the right parameter is reaching the page. That makes debugging easier and gives you confidence that the route structure is correct. -
Keeping this page intentionally small reinforces the lesson’s scope. Right now, the goal is to establish a working URL structure and prevent broken navigation. The actual data-fetching and editing logic can then be added later without needing to rethink how the route itself is set up.
As soon as an app has multiple routes, a simple top navigation often starts to feel cramped. A sidebar is a common dashboard pattern because it gives navigation a stable home while leaving the main content area free to grow.
This code lives in src/app/(dashboard)/layout.tsx. We make it a client layout so it can use usePathname() and determine which navigation item should be highlighted. That active-route feedback is small visually, but it makes a big difference in usability because users can immediately tell where they are in the app.
The most important detail here is the isActive logic. Routes like /tasks/123 should still make “Tasks” appear active, because detail pages are conceptually part of the tasks section.
Because this lesson touches multiple files and introduces reusable components, prompt quality matters even more than before. When there are several related edits, vague prompts make it easier for Codex to change too much or solve the wrong problem.
A strong prompt here should name the exact files you want changed, explain what each file should achieve, and include clear constraints like “don’t touch src/app/api/**.” This keeps the interaction focused and makes it easier to verify that only the intended frontend work was performed.
For example, when extracting TaskRow, you want Codex to create the component in src/components/tasks/TaskRow.tsx, refactor both src/app/(dashboard)/page.tsx and src/app/(dashboard)/tasks/page.tsx, and avoid making “helpful” backend changes. The more precisely you define the scope, the more reliable the output becomes.
-
Tight scope is what keeps Codex fast and dependable in a multi-file lesson like this. You are not asking it to “improve the app”; you are asking it to perform a specific frontend refactor with specific boundaries. That difference is what turns AI assistance into something predictable and useful.
-
Clear success criteria matter just as much as file lists. If you say that
TaskRowshould own the repeated task markup,Buttonshould centralize action styling, and the layout should highlight the active route, then the model has a concrete definition of “done.” Without that, it may make changes that look plausible but do not actually match the lesson goal.
By the end of this lesson, your UI is more maintainable and more app-like. Task rows are rendered through a reusable TaskRow component, which gives you consistent task presentation everywhere. Buttons are rendered through a reusable Button component, which gives your actions a shared style and behavior.
You also established navigation to /tasks/new and /tasks/{id} so the app now has a more stable URL structure, even before the full CRUD experience is implemented. On top of that, your dashboard layout can evolve into a sidebar shell with correct active-route highlighting, which makes the interface feel more structured and easier to use.
Most importantly, you are building a frontend that is easier to extend. That is the real point of this lesson. Instead of just making the current pages work, you are shaping the code so future features can be added without rewriting the same UI patterns again.
Next, you’ll build on this foundation by turning those stub routes into real CRUD experiences, without needing to redesign your component structure from scratch.
