Skip to main content
Career Paths
Concepts
Frontend Project Advanced
The Simplified Tech

Role-based learning paths to help you master cloud engineering with clarity and confidence.

Product

  • Career Paths
  • Interview Prep
  • Scenarios
  • AI Features
  • Cloud Comparison
  • Pricing

Community

  • Join Discord

Account

  • Dashboard
  • Credits
  • Updates
  • Sign in
  • Sign up
  • Contact Support

Stay updated

Get the latest learning tips and updates. No spam, ever.

Terms of ServicePrivacy Policy

© 2026 TheSimplifiedTech. All rights reserved.

BackBack
Interactive Explainer

Build Challenge: TypeScript Shopping Cart

Your first PR at a scale-up: rebuild the shopping cart with TypeScript, Zustand, and component architecture. A senior engineer code-reviews it in 3 days.

Relevant for:Mid-levelSeniorStaff
Why this matters at your level
Mid-level

Build the Zustand store with full TypeScript types. Implement addItem, removeItem, updateQuantity. Write tests for each action. This project is the artifact you reference when asked "tell me about a component you are proud of."

Senior

Add the rendering strategy decision: document why the cart is a Client Component while product listing is a Server Component. Add optimistic updates (update UI immediately, revert on server error). Add a devtools middleware to enable time-travel debugging.

Staff

Design the cart as a micro-frontend: Zustand store exposed via Module Federation, CartBadge and CartDrawer as independent deployable units. Define the team contract: what the store interface looks like, how breaking changes are versioned, and how other teams consume the cart without depending on its internals.

Build Challenge: TypeScript Shopping Cart

Your first PR at a scale-up: rebuild the shopping cart with TypeScript, Zustand, and component architecture. A senior engineer code-reviews it in 3 days.

~6 min read
Be the first to complete!
LIVEReal First PR
Breaking News
Day 1

Engineer reviews existing cart: jQuery, window.cart global, 12 uses of "any", 3 different button implementations.

WARNING
Day 3

PR submitted: TypeScript interfaces for CartItem and CartState, Zustand store, Radix UI Button primitive, full test coverage.

Day 5

Code review from 4 teams: 31 comments. Main debate: should quantity be in Zustand (global) or useState (local)?

WARNING
Day 15

PR merged after architecture alignment. New pattern becomes the team standard for all future cart features.

—uses of "any" in the original cart — each one a potential runtime crash with no compiler warning
—to get a PR merged when 4 teams share a component and architecture decisions are implicit

The question this raises

When 4 teams share one component, what does "good architecture" mean -- and how do you make decisions that survive the next engineer who touches your code?

Test your assumption first

You store cart state in a React Context at the App root. The Header component shows the cart item count. When a user adds an item, the entire page re-renders including the Footer, Sidebar, and unrelated Product components. What is the root cause and what is the correct fix?

Lesson outline

Before TypeScript and components: the jQuery cart

The original cart was not bad code for its time. It was perfect jQuery-era code -- which means it is unmaintainable at scale.

How this concept changes your thinking

Situation
Before
After

Cart state storage

“window.cart = []. Any function anywhere can read or mutate it. When a bug changes the cart, nothing tells you which of 40 functions did it.”

“Zustand store with typed CartItem[]. Only actions defined in the store can mutate state. Every mutation is traceable in React DevTools.”

Rendering cart items

“jQuery: $("#cart").html(renderCartHTML(items)). Replaces the entire cart DOM on every change. Loses focus, re-triggers animations, breaks screen readers.”

“React re-renders only the changed CartItem component. Focus is preserved. Keys ensure the right component updates.”

Type safety

“item.pricee (typo) fails silently at runtime. The bug reaches production. User sees NaN in the cart total.”

“TypeScript: Property "pricee" does not exist on type "CartItem". Caught at compile time, 0 users affected.”

Button component

“Three different button implementations across three teams. Different focus styles, different keyboard behaviour, one missing aria-label.”

“Radix UI Primitive Button: one headless component, ARIA and keyboard baked in, each team brings their own styles via className.”

The naive approach: any, window globals, and prop drilling

The three patterns that guarantee a 30-comment code review on a typed React cart:

cart.ts (before)
1// Pattern 1: any defeats TypeScript completely
2function addToCart(item: any) { // no type safety at all
any: TypeScript cannot catch property typos, wrong types, or missing fields on this parameter
3 window.cart.push(item); // global mutable state
window.cart: any code anywhere can corrupt this. No single source of truth. Untraceable mutations.
4 updateCartUI(); // manual DOM sync — you will forget this somewhere
5}
6
7// Pattern 2: prop drilling through 5 levels
8function App() {
9 const [cart, setCart] = useState([]);
10 return <Page cart={cart} setCart={setCart} />;
11}
12function Page({ cart, setCart }) { // passes down 2 more levels
13 return <Section cart={cart} setCart={setCart} />;
14}
15// ... 3 more levels until CartItem actually needs it
16
17// Pattern 3: interface with optional everything
Prop drilling: adding a new cart action requires touching 5 files. Zustand eliminates this entirely.
18interface CartItem {
19 id?: string; // optional means every consumer must null-check
20 name?: string;
21 price?: number; // price could be undefined — NaN in totals
22 quantity?: number;
Optional price: cart.reduce((sum, item) => sum + item.price, 0) returns NaN if any price is undefined
23}

optional-required-fields

Bug
interface CartItem {
  id?: string;
  name?: string;
  price?: number;
  quantity?: number;
}

const total = items.reduce((sum, item) => sum + item.price, 0);
Fix
interface CartItem {
  id: string;       // required — a cart item without an ID is invalid
  name: string;     // required — user must see what they are buying
  price: number;    // required — total is meaningless without this
  quantity: number; // required — cart item always has a quantity
}

const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);

Optional fields in a CartItem interface are almost always wrong. A cart item with no price or no ID is not a valid cart item -- it is a bug. Required fields force the bug to be a compile error rather than a NaN in production.

What TypeScript and React DevTools show you

TypeScript catches this before runtime

// You type: const total = items.reduce((sum, item) => sum + item.pricee * item.quantity, 0); // TypeScript compiler says: // error TS2339: Property 'pricee' does not exist on type 'CartItem'. // Did you mean 'price'? // Without TypeScript, this silently returns NaN. // The user sees a cart total of NaN. No error in the console. No alert. // Bug is reported by a customer, not caught by the compiler.

React DevTools Profiler: why your cart re-renders 47 times

Open React DevTools Profiler, click Record, then add one item to the cart. If you stored cart state in a Context at the top of the tree, every component that consumes that context re-renders — including the header, the sidebar, and components that do not use the cart at all. Zustand fix: components subscribe to exactly the slice they need. const itemCount = useCartStore(state => state.items.length); // Only re-renders when items.length changes, not on any cart mutation.

The production cart architecture

Four architectural decisions that survive a code review from 4 teams:

DecisionWrong choiceRight choiceWhy
Where does cart state live?React Context at App rootZustand storeContext re-renders everything on every mutation. Zustand components subscribe to exact slices.
What goes in the store vs useState?Everything in ZustandServer data + shared UI in Zustand; ephemeral local state in useStateQuantity stepper open/closed state is local. Items list is shared across Header, Cart, Checkout.
How do you type cart actions?Functions that mutate state directlyZustand actions typed as (state: CartState) => CartStatePure functions are testable and predictable. Direct mutation bypasses Zustand and breaks devtools.
Button component source?Custom-built per teamRadix UI Primitive with custom classNameRadix handles ARIA, keyboard, focus. Teams style it. Zero accessibility bugs from button variations.

The question that resolves every state location debate

Ask: "Does more than one component need this?" If yes, Zustand. If no, useState. The quantity stepper open/close state is needed by one component -- useState. The cart items are needed by Header (count badge), CartDrawer (full list), and Checkout (total) -- Zustand.

How to present this architecture in your interview

Architecture interviews at senior level are not about syntax. They are about decisions. Every line of this cart has a reason. Know the reason.

Questions you will be asked — and what strong answers look like

  • Why Zustand over Redux? — Strong: "Zustand is 1KB, zero boilerplate, no action/reducer split, and components subscribe to slices so only affected components re-render. Redux shines when you need time-travel debugging or have a very large team that benefits from strict action typing. For a cart, Zustand is 50 lines where Redux would be 300." Weak: "Zustand is simpler."
  • Why are all CartItem fields required instead of optional? — Strong: "Optional fields push type errors to runtime. A cart item with no price is invalid by definition -- it should never exist. Required fields make the invalid state unrepresentable. The TypeScript error at compile time is better than a NaN in a user's cart total at 2am." Weak: "It is safer to make them optional in case they are missing."
  • How did you decide what goes in Zustand vs local state? — Strong: "Single question: does more than one component need it? Cart items: yes (Header badge, CartDrawer, Checkout) -- Zustand. Quantity stepper open/closed: no (only the stepper itself) -- useState. This rule makes the decision mechanical, not debatable." Weak: "I put the important stuff in Zustand."
  • How would you render this on the server? — Strong: "Cart state is user-specific and dynamic -- it cannot be static. I would use Next.js with the cart as a Client Component (use client directive) while the rest of the page uses Server Components. The initial cart state loads via a server action or API route, hydrates the Zustand store client-side." Weak: "I would use SSR."
cart.store.ts + CartBadge.tsx
1// Production cart store — the version that survives 4-team code review
2import { create } from 'zustand';
3import { devtools } from 'zustand/middleware';
4
5interface CartItem {
All CartItem fields required: invalid state is unrepresentable at the type level
6 id: string;
7 name: string;
8 price: number;
9 quantity: number;
10 imageUrl: string;
11}
12
13interface CartState {
14 items: CartItem[];
15 addItem: (item: Omit<CartItem, 'quantity'>) => void;
16 removeItem: (id: string) => void;
17 updateQuantity: (id: string, quantity: number) => void;
18 clearCart: () => void;
19 // Derived state as selectors — never stored, always computed
20 totalItems: () => number;
21 totalPrice: () => number;
22}
23
24export const useCartStore = create<CartState>()(
devtools middleware: time-travel debugging in Redux DevTools, free with Zustand
25 devtools( // enables Redux DevTools for time-travel debugging
26 (set, get) => ({
27 items: [],
28
addItem checks for existing item: idempotent, quantity increments instead of duplicate entries
29 addItem: (item) => set(state => {
30 const existing = state.items.find(i => i.id === item.id);
31 if (existing) {
32 return { items: state.items.map(i =>
33 i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
34 )};
35 }
36 return { items: [...state.items, { ...item, quantity: 1 }] };
37 }),
38
39 removeItem: (id) => set(state => ({
40 items: state.items.filter(i => i.id !== id),
41 })),
42
43 updateQuantity: (id, quantity) => set(state => ({
44 items: quantity <= 0
45 ? state.items.filter(i => i.id !== id)
46 : state.items.map(i => i.id === id ? { ...i, quantity } : i),
47 })),
48
49 clearCart: () => set({ items: [] }),
Selectors as functions in the store: computed on demand, no stale derived state to synchronise
50
51 // Selectors: components subscribe to these, not the whole store
52 totalItems: () => get().items.reduce((sum, i) => sum + i.quantity, 0),
53 totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
54 })
55 )
56);
57
Component subscribes to one selector: only re-renders when totalItems() changes, not on any cart mutation
58// Component subscribes only to what it needs — no unnecessary re-renders
59function CartBadge() {
60 const totalItems = useCartStore(state => state.totalItems());
61 return <span aria-label={`${totalItems} items in cart`}>{totalItems}</span>;
62}

Exam Answer vs. Production Reality

1 / 3

Required vs optional TypeScript fields

📖 What the exam expects

Optional fields (field?: type) can be undefined. Required fields (field: type) must always have a value. Use optional for genuinely optional data.

Toggle between what certifications teach and what production actually requires

How this might come up in interviews

System design interviews at senior level ask "how would you architect a shopping cart?" State management interviews ask about global vs local state decisions. TypeScript interviews ask about required vs optional fields.

Common questions:

  • How would you architect global state for a cart that is visible in the header, a drawer, and checkout?
  • Why would you choose Zustand over Redux? When would you choose Redux?
  • Walk me through how TypeScript helped you catch a bug in this project.
  • How do you decide what state belongs in Zustand vs useState?
  • How would you render this cart on the server with Next.js App Router?

Strong answer: Explains the state location rule ("more than one component?"). Knows the Context re-render problem. Uses required fields intentionally. Can explain Zustand selectors. Has considered the SSR/CSR boundary.

Red flags: All state in Context or all state in Zustand with no distinction. Optional fields on required data. Cannot explain why Zustand components subscribe to slices. "I used TypeScript because the job posting asked for it."

Ready to see how this works in the cloud?

Switch to Career Paths for structured paths (e.g. Developer, DevOps) and provider-specific lessons.

View role-based paths

Discussion

Questions? Discuss in the community or start a thread below.

Join Discord

In-app Q&A

Sign in to start or join a thread.

Sign in to track your progress and mark lessons complete.

Discussion

Questions? Discuss in the community or start a thread below.

Join Discord

In-app Q&A

Sign in to start or join a thread.