Skip to main content

Tutorial: Build a Rendering and Data Architecture Lab

Why this tutorial matters

Rendering architecture is difficult to learn from definitions alone. SSR, SSG, ISR, RSC, client rendering, streaming, route loaders, caches, mutations, optimistic UI, and offline states only make sense when you build one app that uses them deliberately.

This tutorial builds a mid-level Commerce Operations Portal. The app has a public catalog, authenticated dashboard, order detail pages, editable customer notes, search filters, and a small offline-capable queue for follow-up tasks. The goal is not to build a beautiful commerce product. The goal is to practice rendering and data decisions until you can explain why each route uses a different strategy.

What you will build

SurfaceRendering/data focus
Public catalogSSG or ISR, cacheable product data, static shell
Product detailISR with revalidation and stale fallback
Authenticated dashboardServer-rendered route shell plus client data refresh
Order detailServer-loaded critical data, client islands for comments/actions
Search and filtersURL search params, schema validation, cache keys
Follow-up tasksMutations, optimistic state, retry, offline queue

Architecture target

Routes
/catalog static or ISR
/catalog/:productId ISR with revalidation
/app/dashboard authenticated server route
/app/orders/:orderId server data + client islands
/app/search URL state + route loader

Data layer
route loaders for first-view data
query cache for client refresh
mutation client for writes
server actions/API routes for trusted writes
shared cache key conventions

React Server Components can render ahead of time or per request depending on framework support. The important lesson is the boundary: server-only data and heavy dependencies should stay off the client, while interactive state stays in client islands.

Milestone 1: Route inventory and rendering matrix

Before coding, write a route matrix.

RouteUser typeFreshnessInteractivityStrategy
/cataloganonymousdaily/hourlylowSSG/ISR
/catalog/:idanonymoushourlymediumISR plus client island for availability
/app/dashboardauthenticatedminutesmediumSSR/server loader
/app/orders/:idauthenticatedlive-ishhighserver critical data plus client islands
/app/searchauthenticatedquery-dependentmediumloader keyed by search params

Acceptance criteria:

  • Every route has a strategy and reason.
  • No route is marked "SSR because dynamic" without naming which data is dynamic.
  • Search params are part of the architecture, not local component state.

Milestone 2: Build route loaders and cache keys

Use a consistent key structure:

const keys = {
catalog: () => ["catalog"],
product: (id: string) => ["product", id],
dashboard: (userId: string) => ["dashboard", userId],
order: (orderId: string) => ["order", orderId],
search: (params: SearchParams) => ["search", normalizeSearch(params)],
};

Rules:

  • cache keys include every input that changes output
  • keys do not include secrets or unstable object identity
  • server and client use compatible key naming
  • revalidation policy is documented per key

Acceptance criteria:

  • Search URL can be copied and reopened.
  • Cache invalidation after mutation updates the right route/query.
  • Duplicate data fetches are visible in logs and removed.

Milestone 3: Server/client component boundary

Create a route where most content is server-rendered but comments/actions are client islands.

OrderDetailPage.server
-> OrderSummary.server
-> FulfillmentTimeline.server
-> CustomerNotes.client
-> FollowUpTaskForm.client

Boundary rules:

  • server components load critical first-view data
  • client components own local input, focus, optimistic state, and browser APIs
  • server-only code never imports into client modules
  • client islands receive serializable props only
  • mutations return structured results, not arbitrary thrown strings

Acceptance criteria:

  • Bundle analysis proves server-only libraries are not in the client bundle.
  • Client islands have loading, pending, success, and error states.
  • Hydration does not shift layout.

Milestone 4: Streaming and suspense states

Split slow secondary data from critical data.

DataStrategy
Order header and statusblock initial render
Timelinestream if slow
Related recommendationsdefer
Audit loglazy tab

Design skeletons with stable dimensions. Avoid a spinner-only page. The user should see the route identity and critical state quickly, even if secondary panels stream later.

Acceptance criteria:

  • First useful view appears before secondary panels.
  • Suspense fallback does not cause layout shift.
  • Errors in secondary panels do not crash the route.

Milestone 5: Mutations and consistency

Implement note editing and follow-up task creation.

Mutation contract:

type MutationResult<T> =
| { ok: true; data: T; version: number }
| { ok: false; code: "validation" | "conflict" | "auth" | "network"; message: string };

Consistency rules:

  • optimistic update only for reversible changes
  • server response reconciles final state
  • conflicts show a resolution UI
  • retries use idempotency keys for create actions
  • failed writes preserve user input

Acceptance criteria:

  • A failed mutation does not lose typed text.
  • Duplicate submit does not create duplicate tasks.
  • Conflict state is testable.

Milestone 6: Offline-lite queue

Add a minimal queue for follow-up tasks:

type QueuedMutation = {
id: string;
type: "create_follow_up";
payload: unknown;
idempotencyKey: string;
createdAt: string;
status: "queued" | "syncing" | "failed";
};

Do not pretend the whole app is offline-first. This is an offline-lite exercise: preserve user intent for one safe mutation type.

Acceptance criteria:

  • User can create a follow-up while temporarily offline.
  • Queue retries with backoff.
  • User can cancel a queued mutation.
  • Server idempotency prevents duplicates.

Milestone 7: Verification

Run these checks:

CheckPurpose
route matrix reviewconfirms each route has strategy
duplicate request logcatches repeated loaders/fetches
bundle analysiscatches server code in client bundle
mutation failure testsverifies state preservation
search param testsverifies URL state and cache keys
slow network testverifies streaming and fallbacks

Implementation sequence

Build the lab in this order:

  1. Create route files and empty layouts.
  2. Add static catalog data and SSG/ISR route.
  3. Add product detail route with revalidation.
  4. Add authenticated dashboard shell with mocked session.
  5. Add route loaders and cache keys.
  6. Add search params with schema validation.
  7. Add order detail server data.
  8. Add notes and follow-up client islands.
  9. Add mutation failure and conflict scenarios.
  10. Add offline-lite queue for one safe mutation.

Do not add caching libraries before the data ownership is clear. A cache cannot fix a confused contract.

Data contract worksheet

For each data source, fill this out:

FieldRequired answer
OwnerWhich backend/domain owns the data?
FreshnessHow stale can it be?
VisibilityAnonymous, authenticated, role-specific, tenant-specific?
First viewDoes the route need this before useful render?
MutationCan the user change it?
InvalidatesWhich keys/routes become stale after change?
Failure UIWhat happens if it cannot load?

Example:

FieldAnswer
OwnerOrders service
FreshnessStatus should be under 30s stale
VisibilityAuthenticated, tenant-scoped
First viewHeader/status yes, audit log no
MutationNotes and follow-up tasks only
Invalidatesorder(orderId), dashboard(userId)
Failure UIHeader error blocks route; audit log error is local

Mid-level extensions

Add these after the core lab works:

  • prefetch product detail route on catalog hover
  • add stale-while-revalidate indicator for dashboard cards
  • add optimistic note update with conflict detection
  • add route-level pending navigation state
  • add background refresh that pauses when tab is hidden
  • add pagination to search with URL state
  • add role-based loader behavior
  • add server-side redirect for unauthorized order access

Each extension should include one failure state. The tutorial is not complete if every feature only works on the happy path.

Capstone review

Answer:

  • Which routes are static, server-rendered, streamed, or client-heavy?
  • Which data is critical for first view?
  • Which data can be stale?
  • What invalidates each cache key?
  • Which mutations are optimistic?
  • Which writes require idempotency?
  • What breaks if the network drops after user input?

Source lens

This tutorial is informed by React Server Components guidance, TanStack Router concepts around route loaders/search params/type safety, and the book's rendering, caching, mutation consistency, and offline-first chapters.