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.
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."
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.
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.
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.
Engineer reviews existing cart: jQuery, window.cart global, 12 uses of "any", 3 different button implementations.
WARNINGPR submitted: TypeScript interfaces for CartItem and CartState, Zustand store, Radix UI Button primitive, full test coverage.
Code review from 4 teams: 31 comments. Main debate: should quantity be in Zustand (global) or useState (local)?
WARNINGPR merged after architecture alignment. New pattern becomes the team standard for all future cart features.
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?
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
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
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 three patterns that guarantee a 30-comment code review on a typed React cart:
1// Pattern 1: any defeats TypeScript completely2function addToCart(item: any) { // no type safety at allany: TypeScript cannot catch property typos, wrong types, or missing fields on this parameter3window.cart.push(item); // global mutable statewindow.cart: any code anywhere can corrupt this. No single source of truth. Untraceable mutations.4updateCartUI(); // manual DOM sync — you will forget this somewhere5}67// Pattern 2: prop drilling through 5 levels8function App() {9const [cart, setCart] = useState([]);10return <Page cart={cart} setCart={setCart} />;11}12function Page({ cart, setCart }) { // passes down 2 more levels13return <Section cart={cart} setCart={setCart} />;14}15// ... 3 more levels until CartItem actually needs it1617// Pattern 3: interface with optional everythingProp drilling: adding a new cart action requires touching 5 files. Zustand eliminates this entirely.18interface CartItem {19id?: string; // optional means every consumer must null-check20name?: string;21price?: number; // price could be undefined — NaN in totals22quantity?: number;Optional price: cart.reduce((sum, item) => sum + item.price, 0) returns NaN if any price is undefined23}
optional-required-fields
interface CartItem {
id?: string;
name?: string;
price?: number;
quantity?: number;
}
const total = items.reduce((sum, item) => sum + item.price, 0);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.
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.
Four architectural decisions that survive a code review from 4 teams:
| Decision | Wrong choice | Right choice | Why |
|---|---|---|---|
| Where does cart state live? | React Context at App root | Zustand store | Context re-renders everything on every mutation. Zustand components subscribe to exact slices. |
| What goes in the store vs useState? | Everything in Zustand | Server data + shared UI in Zustand; ephemeral local state in useState | Quantity stepper open/closed state is local. Items list is shared across Header, Cart, Checkout. |
| How do you type cart actions? | Functions that mutate state directly | Zustand actions typed as (state: CartState) => CartState | Pure functions are testable and predictable. Direct mutation bypasses Zustand and breaks devtools. |
| Button component source? | Custom-built per team | Radix UI Primitive with custom className | Radix 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.
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
1// Production cart store — the version that survives 4-team code review2import { create } from 'zustand';3import { devtools } from 'zustand/middleware';45interface CartItem {All CartItem fields required: invalid state is unrepresentable at the type level6id: string;7name: string;8price: number;9quantity: number;10imageUrl: string;11}1213interface CartState {14items: CartItem[];15addItem: (item: Omit<CartItem, 'quantity'>) => void;16removeItem: (id: string) => void;17updateQuantity: (id: string, quantity: number) => void;18clearCart: () => void;19// Derived state as selectors — never stored, always computed20totalItems: () => number;21totalPrice: () => number;22}2324export const useCartStore = create<CartState>()(devtools middleware: time-travel debugging in Redux DevTools, free with Zustand25devtools( // enables Redux DevTools for time-travel debugging26(set, get) => ({27items: [],28addItem checks for existing item: idempotent, quantity increments instead of duplicate entries29addItem: (item) => set(state => {30const existing = state.items.find(i => i.id === item.id);31if (existing) {32return { items: state.items.map(i =>33i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i34)};35}36return { items: [...state.items, { ...item, quantity: 1 }] };37}),3839removeItem: (id) => set(state => ({40items: state.items.filter(i => i.id !== id),41})),4243updateQuantity: (id, quantity) => set(state => ({44items: quantity <= 045? state.items.filter(i => i.id !== id)46: state.items.map(i => i.id === id ? { ...i, quantity } : i),47})),4849clearCart: () => set({ items: [] }),Selectors as functions in the store: computed on demand, no stale derived state to synchronise5051// Selectors: components subscribe to these, not the whole store52totalItems: () => get().items.reduce((sum, i) => sum + i.quantity, 0),53totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),54})55)56);57Component subscribes to one selector: only re-renders when totalItems() changes, not on any cart mutation58// Component subscribes only to what it needs — no unnecessary re-renders59function CartBadge() {60const totalItems = useCartStore(state => state.totalItems());61return <span aria-label={`${totalItems} items in cart`}>{totalItems}</span>;62}
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
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:
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 pathsSign in to track your progress and mark lessons complete.
Questions? Discuss in the community or start a thread below.
Join DiscordSign in to start or join a thread.