Fetching Order Data
Order History in the Storefront
Welcome back. Now that shoppers can complete checkout, the storefront needs a place where they can review what they have already purchased. That is what this lesson adds: a dedicated orders history screen that fetches placed orders, handles loading and failure states cleanly, and presents each order in a scan-friendly card.
In this lesson, you will build the full frontend path for the /orders page. You will add a small API helper in src/lib/api/orders.ts, wrap that helper in a reusable useOrders() hook inside src/lib/hooks/useOrders.ts, and connect the result to UI components in src/app/orders/page.tsx and src/components/orders/OrdersPageClient.tsx. You will also create the reusable OrderListItem card and expose the new page from the shared navbar so the route feels like a natural part of the app.
Previously
In the previous lesson, you finished the checkout confirmation flow by loading a single order with getOrder(id) and useOrder(orderId), then showing that order on the checkout success screen. That work proved that the app can fetch one specific order after checkout completes.
This lesson builds on that same architecture, but at the collection level. Instead of loading one order by ID, you will now load the full list of orders for the shopper and render them in a dedicated history page. The pattern stays familiar on purpose: small API helper, reusable hook, and a UI layer that branches between loading, error, empty, and successful states.
How the Order History Feature Fits Together
The order history page is a good example of layered frontend design. The API layer is responsible for naming the backend action clearly, the hook layer is responsible for async state and request safety, and the component layer is responsible for choosing what the user sees.
That separation matters because it keeps each file easy to reason about. When a hook calls listOrders(), the reader instantly understands the intent. If the hook instead contained raw fetch() calls, URL strings, and transport details inline, the purpose of the code would be harder to scan. This lesson is as much about clean architecture habits as it is about rendering a page.
Adding the Order Collection API Helper
The file src/lib/api/orders.ts is intentionally small. That is not a limitation; it is a design choice. This module gives the rest of the frontend semantic helpers for order-related requests, so hooks and components can say what they want to do without worrying about the low-level request details.
Here is the file with both order helpers:
-
The import of
apiRequestfrom@/lib/api/clientkeeps this file aligned with the rest of the project’s data layer. The helper already knows how to talk to the backend and return parsed data, so this module does not need to repeat transport logic every time the app needs order data. -
The
Ordertype from@/types/domainmakes the return values explicit and safe. In TypeScript, that matters because the rest of the app gets autocompletion and compile-time checking for real order fields such asid,created_at,status,total_cents, andcurrency. -
listOrders()is the new collection helper that powers the history page. It callsapiRequest<Order[]>('/api/orders'), and theOrder[]generic is important because it tells TypeScript that the request resolves to an array of orders rather than a single order object. -
getOrder(id)remains in the file from the previous lesson because this module is the central place for order reads. Here we use:
to express a single-order read and keep the same clean semantic API that the rest of the frontend expects.
- Keeping both functions in one short file is a strong frontend architecture habit. This module does not try to guess future needs or grow into a large abstraction layer before the UI asks for it; it only exposes the small surface area the current app actually uses.
A short API module like this is often a sign of good design. It means the frontend is naming real business actions clearly without adding unnecessary complexity.
Managing Order History State in a Reusable Hook
The file src/lib/hooks/useOrders.ts is where the order history request becomes a reusable React hook. This is the right place for state, effects, and stale-request protection, because page components should not have to manage those mechanics directly.
The imports and initial state establish the public shape of the hook:
-
useEffect,useRef, anduseStateare the core React tools that make this hook work.useStatestores the current UI-facing values,useEffectruns the fetch when the hook mounts, anduseRefstores mutable data that should survive renders without causing rerenders. -
orders,isLoading, anderrorMessageare the only pieces of state this page needs. Those three values are enough to represent the four meaningful screen states: loading, explicit error, successful empty result, and successful populated result. -
ordersstarts as an empty array because, before the request finishes, the page does not yet have a list to show. Using an empty array instead ofnullmakes list rendering simpler later because the component can safely readorders.lengthonce loading and error conditions are handled. -
isLoadingbegins astrueso the UI can immediately show a loading state on the first render. This is a common and useful React pattern when the hook is responsible for initiating data fetching after mount. -
errorMessagestores a user-facing string rather than the raw thrown error object. That keeps the component clean, because the page only needs display-ready state instead of knowing anything about transport-level error shapes. -
requestIdRefis the stale-request guard. Even though this screen only loads once in its current form, keeping the hook disciplined and consistent with the rest of the project prevents race conditions and reinforces a reusable async pattern.
The next part of useOrders() is the effect that actually loads the orders from the server:
-
useEffect(..., [])tells React to run this effect when the hook’s consumer mounts. Since the dependency array is empty, the order list is fetched once for the current page visit, which matches the behavior of a standard history screen. -
loadOrders()is defined inside the effect because it belongs to that lifecycle step. It keeps the async logic readable while still letting the effect trigger it in the correct React-managed place. -
The
requestIdpattern is important even in seemingly simple hooks. Each new request gets a unique numeric ID, and that ID is compared before updating state so an outdated request cannot overwrite the results of a newer one if the hook is ever reused in a more dynamic context. -
setIsLoading(true)runs before the network request begins so the UI can respond immediately. This makes the hook predictable: whenever a fresh request starts, loading state becomes true first, then one of the success or failure branches follows. -
await listOrders()is exactly the kind of semantic API call this architecture is designed to encourage. The hook expresses business intent clearly and avoids embedding raw endpoint strings like'/api/orders'directly in its implementation. -
On success,
setOrders(response)stores the fetched collection, andsetErrorMessage(null)clears any previous error. That reset matters because a new successful request should remove stale failure UI rather than leaving an old message on screen. -
On failure,
setOrders([])resets the collection to an empty array so the hook does not keep stale data from an earlier success. ThengetErrorMessage(error)converts the thrown value into a readable string that the page can display through a shared error component. -
The
finallyblock clears loading only if the request is still the most recent one. That extra check keeps the state transitions disciplined and prevents older requests from incorrectly ending the loading state after a newer request has already started. -
The
void loadOrders()call is a small but useful TypeScript and React convention. It makes it clear that the async function is intentionally being started without awaiting it in the effect body.
The hook ends by returning a very small interface to the component layer:
-
This return shape is intentionally narrow because good hooks expose state, not implementation details. The page should be able to consume the current results and decide what to render, without reaching inside the hook to manipulate setters manually.
-
Returning only
orders,isLoading, anderrorMessagealso makes the hook easy to reuse elsewhere. A stable, boring interface is a strength in frontend architecture because it reduces surprises and keeps rendering logic in the component layer where it belongs.
This is a great example of a hook being “boring in the best way.” It has clear inputs, predictable state transitions, and no page-specific UI logic leaking into it.
Activating the Orders Route
The route file src/app/orders/page.tsx stays extremely small on purpose. In the Next.js App Router, it is often best for route files to act as thin entry points that hand off rendering to a client component when that client component needs hooks and client-side state.
Here is the complete route file:
-
The route imports
OrdersPageClientfrom@/components/orders/OrdersPageClient, which makes the file’s responsibility very obvious. It exists to activate the/ordersroute, not to own loading logic, display branches, or repeated UI composition. -
Returning only
<OrdersPageClient />keeps route files consistent with the rest of the storefront architecture. This pattern makes the route easy to scan because the reader can immediately see that the real page behavior lives in the dedicated component file. -
This separation becomes even more valuable as pages grow. A thin route and a focused client component produce a clearer codebase than a single route file that mixes routing concerns, data hooks, conditional rendering, and presentation markup all in one place.
Rendering the Orders Page States
The heart of the feature lives in src/components/orders/OrdersPageClient.tsx. This file is a client component because it uses the useOrders() hook, which depends on React state and effects.
The top of the file brings together the shared UI primitives and the custom hook that drives the page:
-
The
'use client'directive is required because this component consumes a hook built withuseStateanduseEffect. In the App Router, that directive tells Next.js this file must run on the client rather than being treated as a server component. -
Importing
PageContainer,SectionHeading,LoadingState,ErrorState, andEmptyStatekeeps the screen visually consistent with the rest of the application. Reusing shared primitives is one of the easiest ways to make a growing app feel polished and coherent. -
useOrders()is the component’s only data dependency, which is a good sign that the hook abstraction is doing its job well. The component does not need to know about request IDs, effect timing, or error parsing; it only needs the stable state values returned by the hook.
The main JSX uses those values to choose the correct UI for each state of the request:
-
PageContainerprovides the outer spacing and layout shell for the page, whileSectionHeadinggives the screen a clear title and supporting description. This makes the feature feel like a first-class destination in the app rather than a temporary utility page. -
The loading branch is checked first with
isLoading ? <LoadingState ... /> : null. This is a very common React conditional rendering pattern, and it keeps the UI honest by showing immediate feedback while the request is still in flight. -
The error branch only appears when loading has finished and
errorMessageexists. That ordering matters because the page should not display loading and error states at the same time; the component is deliberately modeling mutually exclusive request outcomes. -
The empty-success branch is distinct from the error branch, which is an important design choice. “No orders yet” is a valid successful state, not a failure, so the page uses
EmptyStatewith helpful copy and a clear next action back to/shop. -
Wrapping the “Browse products” button in a
Linkmakes the empty state actionable. It gives the shopper a direct way to move from “I have no history yet” into the part of the app where history can actually begin.
The final branch renders the list of real orders once the request succeeds and the collection is not empty:
-
This branch is the populated success state of the feature. The conditions ensure that the list only renders once the request is finished, there is no error, and the array actually contains items.
-
orders.map(...)is the standard React way to turn an array of data into repeated UI elements. EachOrderListItemreceives oneorder, andkey={order.id}gives React a stable identifier so it can reconcile list updates correctly. -
Using a dedicated
OrderListItemcomponent instead of inline markup is an important design decision. It keeps the page component focused on state orchestration while the repeated card pattern lives in its own reusable file. -
The wrapper
divwithspace-y-4creates consistent vertical spacing between cards. Small layout decisions like this matter because order history pages need to be easy to scan quickly, especially when multiple orders appear in sequence.
This file shows a healthy React pattern: the hook owns async state, and the component owns rendering decisions. That separation makes both sides easier to read and maintain.
Designing a Scan-Friendly Order Card
The file src/components/orders/OrderListItem.tsx defines the reusable card for each order in the history list. This component is more than “just markup.” It is the repeated UI pattern that determines how quickly a shopper can understand their order history at a glance.
Here is the complete component:
-
The component accepts one prop,
order, typed asOrder. Keeping the prop typed at the boundary of the component makes the card easier to use correctly, because every caller must provide a real order-shaped object. -
The outer element is a
Link, which makes the entire card clickable. That is an excellent choice for an order history item because the list is not just informational; it is also navigation into deeper order views later. -
Here we use the order’s ID to create the destination path dynamically:
Even if the detailed order screen is expanded later, the list item is already wired to lead naturally into that route structure.
-
The left side of the card answers the “Which order is this?” and “When was it placed?” questions. The label text, the monospace rendering of
order.id, and the formatted placement timestamp together make the identity of the order feel clear and trustworthy. -
formatDateTime(order.created_at)is important because raw timestamps are usually not suitable for end-user interfaces. Formatting utilities let the app present backend data in a way that is readable and consistent across the storefront. -
The right side of the card answers the “How much was it?” and “What state is it in?” questions. Showing
formatMoney(order.total_cents, order.currency)gives a shopper the order total in the same style used elsewhere in the app, andStatusBadgeadds a quick visual cue for the order’s current state. -
The layout classes make the component work well on both smaller and larger screens. On mobile it stacks vertically for readability, and on medium screens and up it shifts into a horizontal arrangement that is faster to scan across a list.
-
The hover styles are subtle but meaningful because they reinforce that the card is interactive. Good order history design is not only about data correctness; it is also about making the page feel responsive and intentional.
A reusable list item like this improves the whole page because it turns raw data into a consistent, readable pattern. When the repeated unit is well designed, the entire feature feels more polished.
Extending the Shared Navbar
The order history page should not feel hidden or disconnected from the rest of the app. The shared navigation bar in src/components/layout/Navbar.tsx is where the new destination becomes part of the storefront shell.
The first important part is the navItems array:
-
Adding
{ href: '/orders', label: 'Orders' }extends the exact same data structure already used for the existing navigation links. That is the right way to add a new destination, because it works with the component’s existing rendering logic instead of introducing a one-off special case. -
The shared
{ href, label }shape keeps the navbar easy to reason about. When new items can be added by extending the array rather than rewriting rendering logic, it is usually a sign that the component API is well designed.
The rendering logic uses that array to create each navigation link and determine which one is active:
-
navItems.map(...)means the navbar does not care whether it is rendering two links, three links, or more later on. It simply iterates over the configuration and produces consistent UI, which is exactly what you want in a shared shell component. -
The
activecalculation is especially useful because the new Orders link automatically participates in the same active-link behavior as Home and Shop. There is no need for separate branching logic just because this feature introduces a new destination. -
Using
pathname.startsWith(item.href)for non-root paths means/orderswill still count as active for nested order routes such as/orders/123. That keeps navigation highlighting accurate as the order section grows beyond the history page.
The navbar also includes the cart badge logic, and it is important that adding Orders does not disrupt that existing behavior:
-
useCart()still providesitemCountandisReady, which means the navbar continues to own its cart badge responsibility exactly as before. Extending the navigation should not regress existing shared-shell features, and this code preserves that expectation. -
The badge shows
itemCountonly whenisReadyis true, otherwise it renders'...'. That small detail prevents the UI from showing incorrect cart data before the cart state has finished initializing on the client. -
This is a useful broader lesson about shared components: when you add new functionality, prefer extending the existing pattern instead of disturbing unrelated responsibilities. The Orders link belongs inside the navbar’s existing structure, not beside it as a special exception.
Why the Page Uses a Hook Instead of Fetching Inline
It is worth pausing on one architecture choice in this lesson: the page does not fetch orders directly inside OrdersPageClient. Instead, the request lives in useOrders(), and that hook calls listOrders() from the API layer.
That layered approach makes the code easier to read because each part has one responsibility. listOrders() names the backend action, useOrders() handles the asynchronous state transitions, and OrdersPageClient focuses entirely on rendering the correct UI for the current state. When those responsibilities are separated, the codebase becomes easier to extend later with features like refetching, filtering, or deeper order routes.
This is also why the hook returns only { orders, isLoading, errorMessage }. A page component should consume state, not reach inside the hook and manually control its internals. That narrow contract makes the hook reusable and keeps rendering logic in the component layer where React developers expect to find it.
Recap
In this lesson, you added the full order history path to the storefront.
You started in src/lib/api/orders.ts, where listOrders() was added as a small semantic helper that returns apiRequest<Order[]>('/api/orders'). From there, src/lib/hooks/useOrders.ts wrapped that helper in a reusable React hook that tracks orders, isLoading, and errorMessage, uses useEffect() to fetch on mount, and protects state updates with requestIdRef.
After that, src/app/orders/page.tsx activated the route with a thin page file, while src/components/orders/OrdersPageClient.tsx used the hook to branch cleanly between loading, error, empty, and populated success states. src/components/orders/OrderListItem.tsx turned each order into a clickable, scan-friendly card that shows the order ID, placement time, total, and status. Finally, src/components/layout/Navbar.tsx added the Orders destination by extending the existing navItems pattern without disrupting the cart badge behavior.
The main takeaway is that even a straightforward history screen benefits from disciplined layering. A tiny API helper keeps requests semantic, a focused hook keeps async behavior predictable, and a dedicated page component keeps the UI easy to read. That combination is what makes the new /orders section feel like a natural, polished part of the storefront.
