Built May 9, 2026

Train for the screen like it is a product build.

The goal is not to memorize trivia. The goal is to repeatedly turn an ambiguous prompt into a working, accessible, resilient UI while explaining clean state, effects, data loading, performance, and tradeoffs.

React + TypeScript Practical UI drills Async races A11y + CSS Realtime design Performance

READ ORDER: start with the interview target, learn the C-S-A-R-E loop, study the React patterns, then run the exercises as timed lab work.

field manual index

Eleven chapters, meant to be read in order.

12 exercises 9 scripts 10 source links 1 cram sheet
fig_001 / render loop
fig_002 / async pipeline

01 / target

What This Prep Optimizes For

Modern web-engineering rounds tend to emphasize TypeScript, Node.js, React, CSS, UI/UX quality, performance, security, offline functionality, high-concurrency systems, low latency, and real-time experiences. This guide optimizes for the practical version of that: chat-style UIs, screenshot-to-UI work, async handling, accessibility, performance, and concise explanations.

Lead withData shape, state shape, edge states, then first working slice.
Win byExplaining why a value is state, derived, ref, or effect-owned.
AvoidSilent coding, duplicated state, custom div controls, and async without cleanup.
Close withTests, accessibility, performance measurement, and failure modes.

Likely live-coding signals

  • Can you turn vague product requirements into clear component state?
  • Can you build the UI in small working slices instead of stalling on architecture?
  • Can you handle loading, empty, error, optimistic, and disabled states?
  • Can you prevent stale async results from overwriting newer UI?
  • Can you write semantic HTML and keyboard-friendly interactions?
  • Can you talk through performance without overusing memoization?

The shape of a strong answer

  1. Restate the product goal in one sentence.
  2. Name the data model and minimum state.
  3. Implement the first working version quickly.
  4. Add edge states and accessibility affordances.
  5. Harden async, keyboard, and mobile behavior.
  6. End with tradeoffs and next tests.
Interview north star In a practical web round, saying "I am keeping only the user's inputs and selected IDs in state; everything else is derived in render" is often more valuable than introducing a bigger architecture too early.

02 / framework

C-S-A-R-E: The Repeatable Build Framework

Use this on every prompt. It keeps you from jumping straight into JSX before you know what the UI must remember, what it can derive, and what external systems it must synchronize with.

core rule

Clarify what can change, store only the source of truth, derive everything else.

ClarifyAsk the one question that changes code: local vs server, single vs multi-select, realtime vs snapshot.
StateWrite state names before JSX so your implementation has a spine.
ActionsName handlers by user intent, not DOM events: submit, retry, choose, dismiss.
EffectsUse only for external sync: fetch, subscription, timer, focus, scroll, storage.

Clarify

Ask only questions that change implementation. "Should search be local or server-backed?" is useful. "What color?" usually is not.

  • What data shape do I receive?
  • What user actions change state?
  • What edge states are required?
  • Keyboard and mobile expectations?

State

Write the minimum source of truth before rendering. Avoid storing filtered lists, counts, or labels that can be calculated.

  • Inputs: query, draft message, selected tab.
  • Server: status, data, error.
  • UI: open menu ID, focused index.
  • Refs: DOM nodes, latest request ID.

Actions

Name handlers by user intent. Keep state transitions close to the action that caused them.

  • onQueryChange
  • onSubmitMessage
  • onSelectResult
  • onRetry

Render

Derive UI from state in the render path. Show the state machine clearly: idle, loading, empty, error, success.

Effects

Use effects only to synchronize with something outside React: network, browser APIs, timers, focus, scroll, subscriptions, storage.

C-S-A-R-E scratchpad
/*
Clarify:
- Is data local or server-backed?
- What are loading, empty, error, and disabled states?
- What keyboard/mobile behavior matters?

State:
- query: string
- selectedId: string | null
- status: "idle" | "loading" | "success" | "error"
- data: Item[]
- error: string | null

Actions:
- changeQuery(next)
- selectItem(id)
- retry()
- clear()

Render:
- visibleItems = filter/sort data during render
- status controls loading/empty/error/success

Effects:
- fetch when query changes
- abort stale fetches
- focus/scroll sync when needed
*/

03 / react 0 to hero

React Concepts You Should Be Able To Build With

These are the concepts that show up in product-style screens. Learn them through implementation, not definition recitation.

Default moveUse controlled state, render from it, then add edge states.
State testIf it can be computed from props or state, derive it during render.
Effect testIf nothing outside React is involved, you probably do not need an effect.
TypeScriptType props, event handlers, reducer actions, and async state unions.
Components, props, and render thinking

Components are functions that turn props and state into UI. A good interview component has a small public surface: clear props, predictable callbacks, and no hidden dependency on global state.

High-yield answer

"I keep components small by making their inputs explicit: data in through props, user intent out through callbacks."

  • Trap: mixing fetching, selection, formatting, and layout in one component too early.
  • Upgrade: split only after the first slice works or the prop boundary becomes obvious.
Reusable component shape
type EmptyStateProps = {
  title: string;
  body: string;
  actionLabel?: string;
  onAction?: () => void;
};

function EmptyState({ title, body, actionLabel, onAction }: EmptyStateProps) {
  return (
    <section aria-labelledby="empty-title" className="empty">
      <h2 id="empty-title">{title}</h2>
      <p>{body}</p>
      {actionLabel && onAction ? (
        <button type="button" onClick={onAction}>{actionLabel}</button>
      ) : null}
    </section>
  );
}
State as source of truth

Store values that the user or server can change independently. Do not store values that can be calculated from existing state during render. This reduces bugs where two pieces of state drift apart.

High-yield answer

"State is the minimum set of facts that can change independently. Filtered lists, counts, labels, and selected objects are derived."

  • Trap: useEffect plus setFilteredItems for local search.
  • Upgrade: if filtering is expensive, memoize after measuring; do not duplicate source of truth.
Store in stateDerive in render
queryfilteredItems
selectedIdselectedItem
sortKey, sortDirsortedRows
messages, draftcanSubmit, grouped messages
status, data, errorempty/success labels and counts
Derived filtering, no effect needed
function UserSearch({ users }: { users: User[] }) {
  const [query, setQuery] = useState("");

  const visibleUsers = users.filter((user) => {
    const haystack = `${user.name} ${user.handle}`.toLowerCase();
    return haystack.includes(query.trim().toLowerCase());
  });

  return (
    <>
      <label htmlFor="user-search">Search users</label>
      <input
        id="user-search"
        value={query}
        onChange={(event) => setQuery(event.currentTarget.value)}
      />

      {visibleUsers.length === 0 ? (
        <p role="status">No users match "{query}".</p>
      ) : (
        <ul>
          {visibleUsers.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </>
  );
}
Events and forms

Put event-specific logic in event handlers. If a user clicked Submit, your handler has the context. Avoid "watching" state with an effect to infer what happened.

High-yield answer

"Submit belongs in the submit handler because the event is the source of truth for that action."

  • Trap: toggling isSubmitted and using an effect to fire the request.
  • Upgrade: disable the button while saving, preserve user input on failure, and announce errors.
Typed form submit
function InviteForm({ onInvite }: { onInvite: (email: string) => Promise<void> }) {
  const [email, setEmail] = useState("");
  const [status, setStatus] = useState<"idle" | "saving" | "done" | "error">("idle");

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const nextEmail = email.trim();
    if (!nextEmail) return;

    setStatus("saving");
    try {
      await onInvite(nextEmail);
      setEmail("");
      setStatus("done");
    } catch {
      setStatus("error");
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="invite-email">Email</label>
      <input
        id="invite-email"
        type="email"
        value={email}
        onChange={(event) => setEmail(event.currentTarget.value)}
      />
      <button disabled={status === "saving" || !email.trim()}>
        {status === "saving" ? "Inviting..." : "Invite"}
      </button>
      {status === "error" ? <p role="alert">Could not send invite.</p> : null}
    </form>
  );
}
Effects, refs, and external sync

Effects are for synchronization with external systems. Common interview uses: fetch data, subscribe to events, manage timers, sync focus/scroll, and attach browser listeners with cleanup.

High-yield answer

"This effect synchronizes React with an external system, and cleanup reverses the subscription or cancels stale work."

  • Trap: using effects to calculate render data or mirror props into state.
  • Upgrade: call out dependencies, cleanup, and stale closure behavior while coding.
  • Good effect: fetch when query changes and cancel stale work.
  • Good effect: scroll a message list when a new message appears.
  • Good effect: install a keydown listener and remove it on cleanup.
  • Suspicious effect: copy props.items into visibleItems state.
  • Suspicious effect: submit an API call because isSubmitted became true.
When to use useReducer

Start with useState. Reach for useReducer when many related transitions update the same state object and action names make the code easier to reason about. Do not use it to look advanced.

High-yield answer

"I would keep useState until transitions start coupling together; then a reducer makes allowed state changes explicit."

  • Trap: reducer ceremony for a single boolean or one input.
  • Upgrade: model async state as a union so impossible combinations are harder to represent.
Reducer only when transitions get noisy
type State = {
  status: "idle" | "loading" | "success" | "error";
  items: Item[];
  error: string | null;
};

type Action =
  | { type: "load" }
  | { type: "success"; items: Item[] }
  | { type: "error"; message: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "load":
      return { ...state, status: "loading", error: null };
    case "success":
      return { status: "success", items: action.items, error: null };
    case "error":
      return { ...state, status: "error", error: action.message };
    default:
      return state;
  }
}

04 / core patterns

Copyable Patterns To Study Cold

These are intentionally small. In a live round, you want reusable muscle memory for the pieces you will combine into a product UI.

AsyncLoading, empty, error, abort, stale guard, retry, disabled state.
InputControlled value, debounce only the side effect, keep typing immediate.
ListsStable keys, derived filtering/sorting, keyboard selection, virtualization if large.
ChatOptimistic append, stream state, stop/retry, scroll sync, failure visibility.
Async fetch with AbortController and stale-result guard

Use cancellation to stop work when possible, and a latest-request guard to prevent older responses from overwriting newer state if responses resolve out of order.

High-yield answer

"The abort prevents wasted work after unmount or dependency change; the request ID prevents out-of-order responses from winning."

  • Trap: only using AbortController and assuming it solves every race.
  • Upgrade: preserve prior data during refresh when that feels better than flashing empty UI.
useUsers with cancellation and latest request
function useUsers(url: string) {
  const [state, setState] = useState<{
    status: "idle" | "loading" | "success" | "error";
    users: User[];
    error: string | null;
  }>({ status: "idle", users: [], error: null });

  const latestRequestId = useRef(0);

  useEffect(() => {
    const requestId = ++latestRequestId.current;
    const controller = new AbortController();

    setState((prev) => ({ ...prev, status: "loading", error: null }));

    async function run() {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const users = (await response.json()) as User[];

        if (requestId === latestRequestId.current) {
          setState({ status: "success", users, error: null });
        }
      } catch (error) {
        if (controller.signal.aborted) return;
        if (requestId === latestRequestId.current) {
          setState({
            status: "error",
            users: [],
            error: error instanceof Error ? error.message : "Unknown error"
          });
        }
      }
    }

    run();
    return () => controller.abort();
  }, [url]);

  return state;
}

Say out loud: "The abort handles cleanup, and the request ID handles response ordering."

Debounced autocomplete
High-yield answer

"The input stays immediate; only the query used for fetching is debounced."

  • Trap: debouncing the controlled input value and making typing feel laggy.
  • Upgrade: combine debounce with abort, latest request guard, highlighted matches, and keyboard navigation.
useDebouncedValue
function useDebouncedValue<T>(value: T, delayMs: number) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timerId = window.setTimeout(() => setDebounced(value), delayMs);
    return () => window.clearTimeout(timerId);
  }, [value, delayMs]);

  return debounced;
}

function SearchBox() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebouncedValue(query, 250);
  const url = `/api/search?q=${encodeURIComponent(debouncedQuery)}`;
  const results = useSearchResults(url);

  return (
    <div>
      <label htmlFor="search">Search</label>
      <input
        id="search"
        value={query}
        onChange={(event) => setQuery(event.currentTarget.value)}
      />
      <Results state={results} />
    </div>
  );
}
Chat message submit flow
High-yield answer

"I append the user message immediately with a temporary ID, then reconcile it with the server response or mark it retryable."

  • Trap: blocking the whole composer while the server responds and losing the draft on failure.
  • Upgrade: split draft state from send state so typing remains responsive during streaming.
Chat composer with optimistic local append
function ChatComposer() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [draft, setDraft] = useState("");
  const [isSending, setIsSending] = useState(false);

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const text = draft.trim();
    if (!text || isSending) return;

    const optimisticMessage: Message = {
      id: crypto.randomUUID(),
      role: "user",
      text,
      status: "sending"
    };

    setDraft("");
    setMessages((prev) => [...prev, optimisticMessage]);
    setIsSending(true);

    try {
      const saved = await sendMessage(text);
      setMessages((prev) =>
        prev.map((message) =>
          message.id === optimisticMessage.id ? saved : message
        )
      );
    } catch {
      setMessages((prev) =>
        prev.map((message) =>
          message.id === optimisticMessage.id
            ? { ...message, status: "error" }
            : message
        )
      );
    } finally {
      setIsSending(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="draft">Message</label>
      <textarea
        id="draft"
        value={draft}
        onChange={(event) => setDraft(event.currentTarget.value)}
      />
      <button disabled={!draft.trim() || isSending}>Send</button>
    </form>
  );
}
Scroll to bottom with refs
High-yield answer

"A ref is for the DOM node, not render data; the effect runs only when message count changes."

  • Trap: scrolling on every render or storing DOM nodes in state.
  • Upgrade: avoid auto-scroll if the user has intentionally scrolled up to read history.
Message list scroll sync
function MessageList({ messages }: { messages: Message[] }) {
  const endRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    endRef.current?.scrollIntoView({ block: "end" });
  }, [messages.length]);

  return (
    <section aria-label="Conversation">
      <ol>
        {messages.map((message) => (
          <li key={message.id}>{message.text}</li>
        ))}
      </ol>
      <div ref={endRef} />
    </section>
  );
}
Keyboard navigation for command palette
High-yield answer

"The active index is UI state; Arrow keys clamp it, Enter chooses it, and Escape should close while restoring focus."

  • Trap: clickable results with no keyboard path.
  • Upgrade: reset active index when the filtered list changes and announce empty results.
Arrow key focused index
function CommandResults({ items, onChoose }: Props) {
  const [activeIndex, setActiveIndex] = useState(0);

  function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
    if (event.key === "ArrowDown") {
      event.preventDefault();
      setActiveIndex((index) => Math.min(index + 1, items.length - 1));
    }
    if (event.key === "ArrowUp") {
      event.preventDefault();
      setActiveIndex((index) => Math.max(index - 1, 0));
    }
    if (event.key === "Enter") {
      const item = items[activeIndex];
      if (item) onChoose(item);
    }
  }

  return (
    <div role="listbox" tabIndex={0} onKeyDown={handleKeyDown}>
      {items.map((item, index) => (
        <button
          key={item.id}
          type="button"
          role="option"
          aria-selected={index === activeIndex}
          onClick={() => onChoose(item)}
        >
          {item.label}
        </button>
      ))}
    </div>
  );
}
Highlighted text with semantic mark
High-yield answer

"This is pure derived rendering from text and query, so no state or effect is needed."

  • Trap: injecting highlighted HTML strings and creating XSS risk.
  • Upgrade: handle multiple matches only after the single-match version is correct.
Highlight query matches
function HighlightedText({ text, query }: { text: string; query: string }) {
  const needle = query.trim();
  if (!needle) return <>{text}</>;

  const lowerText = text.toLowerCase();
  const lowerNeedle = needle.toLowerCase();
  const index = lowerText.indexOf(lowerNeedle);

  if (index === -1) return <>{text}</>;

  return (
    <>
      {text.slice(0, index)}
      <mark>{text.slice(index, index + needle.length)}</mark>
      {text.slice(index + needle.length)}
    </>
  );
}

05 / web fundamentals

Web Fundamentals Interviewers Actually Notice

Practical frontend rounds reward people who know the browser as a platform: HTML, CSS, JS, network, performance, accessibility, security, and realtime constraints.

web rule

Explain what the browser is doing, not just what React is doing.

BrowserNetwork request, parse, render path, event loop, compositing.
HTMLSemantic controls, labels, focus order, status/alert regions.
PerformanceLCP, CLS, INP, bundle cost, render cost, network waterfall.
SecurityXSS, CSRF, token storage, CORS, rate limits, private keys server-side.

Request path

URL parse -> DNS -> TCP/TLS -> HTTP request -> response headers -> body stream.

  • Know where caching, CORS, cookies, redirects, and compression fit.
  • Say: "Fetch resolves on headers; I still check response.ok."

Render path

HTML -> DOM, CSS -> CSSOM, combine -> layout -> paint -> composite.

  • Layout changes are expensive; transform and opacity are cheaper to animate.
  • Images and fonts can move layout if dimensions are not reserved.

Runtime

Synchronous JS blocks input and rendering until the call stack clears.

  • Promises queue microtasks; timers and events queue tasks.
  • Use functional state updates when next state depends on previous state.

Controls

Native controls carry keyboard, focus, names, and form behavior.

  • Use button for actions, anchor for navigation, label for inputs.
  • Announce async states with role="status" or role="alert".

Threats

Assume user input, URLs, HTML, auth tokens, and expensive endpoints can be abused.

  • Avoid raw HTML injection; protect cookie-auth mutations from CSRF.
  • Keep private keys server-side and rate-limit expensive actions.

HTML and accessibility

  • Use semantic elements: button for actions, a for navigation, headings in order.
  • Every input needs a programmatic label through label htmlFor or aria-label.
  • Use role="status" for passive async updates and role="alert" for urgent errors.
  • Keyboard users need visible focus, logical tab order, Escape behavior for dialogs, and arrow-key support for composite widgets.
  • Dialogs should trap focus, restore focus on close, and be dismissible.

CSS layout

  • Use flex for one-dimensional alignment, grid for two-dimensional layout.
  • Use minmax(0, 1fr) in grids to prevent overflowing content.
  • Prefer responsive constraints over viewport-scaled text.
  • Animate transform and opacity, not layout properties.
  • Use position: sticky for stable local navigation.
JavaScript runtime fundamentals
High-yield answer

"Long synchronous JavaScript blocks input and paint. Promises run as microtasks before the next task, so I avoid heavy work on the main thread."

  • Trap: assuming setTimeout(..., 0) runs before promise callbacks.
  • Upgrade: split heavy work, defer non-critical work, or move CPU-heavy logic to a worker.
  • Call stack: synchronous functions execute to completion.
  • Task queue: timers, events, and network callbacks wait for the stack to clear.
  • Microtasks: promise callbacks run before the browser takes the next task.
  • Rendering: long JS blocks input and painting, so heavy work should be split, deferred, or moved to a worker.
  • Closures: handlers capture values from the render they were created in; use functional updates for next state based on previous state.
Functional update avoids stale captured state
setMessages((previousMessages) => [
  ...previousMessages,
  { id: crypto.randomUUID(), text: draft }
]);
HTTP, fetch, CORS, and caching
High-yield answer

"Fetch rejects on network failure, not HTTP error status, so I check response.ok and model error UI explicitly."

  • Trap: treating CORS as authentication; it is browser-enforced read permission.
  • Upgrade: name the cache layer you mean: browser, CDN, app cache, or server cache.
  • HTTP methods: GET reads, POST creates/actions, PUT replaces, PATCH partially updates, DELETE removes.
  • Status codes: 2xx success, 3xx redirect/cache, 4xx client issue, 5xx server issue.
  • Fetch: resolves when response headers arrive. Check response.ok; HTTP errors do not automatically throw.
  • CORS: browser-enforced policy for cross-origin reads. Server must opt in with the right headers.
  • Cache: distinguish browser cache, CDN cache, application cache, and server data cache.
  • Credentials: cookies require correct credentials, same-site behavior, secure flags, and CORS settings.
Fetch mental model
async function getJson<T>(url: string, signal?: AbortSignal): Promise<T> {
  const response = await fetch(url, {
    method: "GET",
    headers: { Accept: "application/json" },
    signal
  });

  // fetch only rejects for network-level failures, not HTTP 404/500.
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }

  return (await response.json()) as T;
}
Performance
High-yield answer

"I would measure production behavior first, then reduce the biggest cost: network, bundle, render, layout, or main-thread work."

  • Trap: reaching for memo before proving the render is expensive.
  • Upgrade: connect each fix to a metric: LCP, CLS, INP, TBT, request count, or JS bytes.
  • Measure first: use production build, throttled CPU/network, and real devices when possible.
  • Core metrics: LCP for main content, CLS for layout stability, INP for interaction responsiveness.
  • Render cost: reduce unnecessary state, avoid expensive work in render, virtualize long lists.
  • Network cost: compress, cache, preconnect when justified, lazy-load non-critical assets.
  • Bundle cost: split by route or feature, avoid shipping admin or editor code to all users.
  • React cost: memoize only proven expensive computations or unstable props that cause real re-render issues.
Split heavy browser work
function scheduleChunkedWork(items: Item[], visit: (item: Item) => void) {
  let index = 0;

  function runChunk(deadline: IdleDeadline) {
    while (index < items.length && deadline.timeRemaining() > 4) {
      visit(items[index]);
      index += 1;
    }

    if (index < items.length) {
      requestIdleCallback(runChunk);
    }
  }

  requestIdleCallback(runChunk);
}
Security basics
High-yield answer

"Client code is public, user input is hostile, and browser protections only help if the server configuration is correct."

  • Trap: putting private API keys in bundled JavaScript or trusting hidden form fields.
  • Upgrade: mention CSP, SameSite cookies, CSRF protection, validation, rate limits, and audit logs.
  • XSS: do not inject raw HTML; sanitize user content if unavoidable.
  • CSRF: same-site cookies plus CSRF tokens for state-changing cookie-authenticated requests.
  • Auth storage: know tradeoffs between HTTP-only cookies and JS-accessible tokens.
  • Secrets: never put private API keys in client code.
  • CSP: defense-in-depth to restrict script, style, image, and connection sources.
  • Rate limits: protect expensive endpoints and realtime systems from abuse.
Realtime and offline
High-yield answer

"I choose the simplest transport that matches directionality and latency: polling, SSE, or WebSocket, then design reconnect and backpressure."

  • Trap: rendering every streamed event one by one and making the UI janky.
  • Upgrade: batch stream updates, preserve drafts offline, queue mutations, and reconcile after reconnect.
  • Polling: simplest, but inefficient for low-latency updates.
  • SSE: server-to-client streaming over HTTP, good for feeds and AI token streams.
  • WebSockets: bidirectional realtime channel, good for chat, presence, collaborative state, and live dashboards.
  • Offline: queue mutations, persist drafts, show sync state, reconcile on reconnect.
  • Backpressure: do not render every event if the stream is faster than the UI can update.

06 / frontend system design

System Design For Web Engineers

For frontend-heavy system design, you are designing the user experience, data flow, client architecture, reliability model, performance profile, and operating plan. The backend matters, but keep tying choices back to user-visible behavior.

StartUsers, devices, core flows, scale, latency, and non-goals.
ModelEntities, stable IDs, permissions, timestamps, ordering, pagination.
ClientServer cache, local UI state, form drafts, optimistic mutation state.
OperateMetrics, logs, retries, feature flags, rollout, abuse controls.

Architecture checklist

  • Users and use cases: who uses it, how often, on what devices?
  • Core flows: read, create, update, delete, share, search, realtime updates.
  • Data model: entities, IDs, timestamps, permissions, derived views.
  • APIs: endpoints, pagination, filtering, streaming, idempotency, errors.
  • Client state: server cache, local UI state, forms, optimistic updates.
  • Realtime: polling/SSE/WebSockets, presence, ordering, reconnects.
  • Performance: initial load, interactions, long lists, media, cache.
  • Reliability: retries, offline, conflicts, observability, rollout.

Good tradeoff language

  • "I would start with server pagination and add cursor-based infinite scroll once product requires it."
  • "I would keep optimistic updates local and reconcile with server IDs on success."
  • "For token streaming, SSE is simpler if the client only receives updates; WebSockets if the client also sends frequent events."
  • "I would virtualize the list once rows exceed what the browser can comfortably render."
  • "I would isolate critical input state from stream updates so typing stays responsive."
Design prompt: AI chat UI
High-yield answer

"For chat, I care most about perceived latency, stream reliability, draft safety, retryability, and keeping input responsive while tokens arrive."

  • Trap: treating a streamed assistant response like a single final message.
  • Upgrade: include stop generation, reconnect, chunk ordering, markdown cost, and abuse controls.

Requirements

  • Send messages, stream assistant responses, show pending and failed states.
  • Persist conversation history and allow switching conversations.
  • Support attachments later, but scope first version to text.
  • Keyboard submit, multiline drafts, mobile layout, screen-reader status updates.

Design

  • Entities: Conversation, Message, User, Attachment, StreamEvent.
  • Client state: selectedConversationId, draftByConversation, visible messages, stream status, error.
  • API: GET conversations, GET messages cursor, POST message, stream assistant response via SSE or WebSocket.
  • Optimistic UX: append user message immediately with temporary ID; replace with saved ID.
  • Ordering: server timestamp plus sequence number for streamed chunks.
  • Failure: retry failed sends, regenerate failed assistant responses, preserve drafts.
  • Performance: virtualize long histories, chunk stream rendering, memoize markdown rendering only when proven expensive.
  • Observability: message send latency, stream start latency, dropped connection rate, error rates by endpoint.
Design prompt: realtime analytics dashboard
High-yield answer

"The UI should render an initial snapshot, then merge live deltas in batches with an obvious freshness and reconnect state."

  • Trap: re-rendering every chart on every event.
  • Upgrade: aggregate by frame or interval, then refetch a snapshot after reconnect.
  • Scope: live metrics, filters, time ranges, drill-down table, export.
  • Data: metric definitions, dimensions, aggregates, raw events, user permissions.
  • Transport: initial snapshot over HTTP, live deltas over WebSocket or SSE.
  • Backpressure: aggregate events client-side per animation frame or per second.
  • Consistency: show "last updated" and reconnect state; refetch snapshot after reconnect.
  • Performance: avoid re-rendering entire chart on every event; batch updates.
  • Security: authorize every metric query; do not leak tenant IDs in client filters.
Design prompt: realtime social feed
High-yield answer

"Separate entity cache from feed order, use cursor pagination, and avoid moving content under the reader when realtime posts arrive."

  • Trap: offset pagination for a fast-changing feed.
  • Upgrade: optimistic likes with rollback, media lazy-loading, and mutation/error observability.
  • Core flow: load home feed, paginate, refresh, compose, like, reply, repost.
  • API: cursor-based feed, mutation endpoints, media upload flow, ranking metadata hidden from client.
  • Optimistic updates: likes and reposts update immediately with rollback on failure.
  • Cache: separate identity cache for users/posts from feed order cache.
  • Realtime: show "new posts" affordance rather than shifting content while reading.
  • Perf: virtualized feed, image lazy-loading, prefetch next cursor near viewport end.
  • A11y: heading landmarks, actionable buttons with labels, focus retention after mutations.
System design answer skeleton
1. Clarify product goal, users, devices, and scale.
2. List core flows and non-goals for v1.
3. Define data model and stable IDs.
4. Sketch APIs: reads, writes, pagination, streaming, errors.
5. Split client state:
   - server cache
   - local UI state
   - form/draft state
   - optimistic mutation state
6. Cover edge states: loading, empty, error, offline, reconnecting.
7. Discuss performance: initial load, list virtualization, batching, cache.
8. Discuss accessibility and security.
9. Name observability metrics and rollout plan.
10. Close with tradeoffs and future work.

07 / practice problems

Practical Drills

Work down the list like lab exercises. Timebox one, ship the smallest working version, then use the twist as the interviewer follow-up.

PassA working slice with correct state and no obvious async bug.
StrongEdge states, keyboard path, disabled states, and clear explanation.
ExcellentHandles follow-up twist without rewriting the architecture.
ReviewAfter each drill, write the state model and one bug you prevented.
30 minReactA11y

Tabs With Keyboard Support

Build tabs where clicking and arrow keys switch panels. Include selected state, focus styling, and semantic roles.

High-yield focus

State is selected tab ID. Derive active panel and ARIA state from it.

Acceptance criteria
  • Only active panel is visible.
  • ArrowLeft and ArrowRight move active tab.
  • Tabs are buttons with clear labels.
  • State is just selected tab ID.
Twist

Tabs come from API data and one tab may be disabled.

35 minReactState

Toast Queue

Build a toast system with add, auto-dismiss, manual dismiss, and stacked rendering.

High-yield focus

Each toast needs a stable ID and timer cleanup; array updates stay immutable.

Acceptance criteria
  • Each toast has stable ID.
  • Dismiss removes one toast.
  • Timers clean up on unmount.
  • No array mutation.
Twist

Pause auto-dismiss while hovering the toast region.

45 minReactA11y

Modal Confirm Dialog

Build a destructive confirm dialog with open, cancel, confirm, Escape close, focus restore, and disabled saving state.

Acceptance criteria
  • Dialog title and body are announced.
  • Escape closes unless saving.
  • Focus returns to opener.
  • Confirm button shows pending state.
Twist

Require typing a resource name before confirm is enabled.

45 minAsyncReact

Autocomplete Search

Build debounced search against an API with loading, empty, error, cancellation, highlighted matches, and keyboard selection.

High-yield focus

Keep raw input instant, debounce fetch query, abort stale requests, and guard response ordering.

Acceptance criteria
  • Debounce input by 200-300ms.
  • Abort stale fetches.
  • Older responses cannot overwrite newer results.
  • Enter selects active result.
Twist

Add recent searches from localStorage and merge them above server results.

40 minReactForms

Stepper Form

Build a three-step form with validation, back/next, review screen, and submit state.

Acceptance criteria
  • Current step is source of truth.
  • Cannot advance with invalid fields.
  • Back preserves draft values.
  • Review derives from form state.
Twist

Allow saving a draft to localStorage and restoring after refresh.

50 minReactData UI

Sortable Filterable Table

Build a table with query filtering, sortable columns, status filters, row selection, and empty states.

Acceptance criteria
  • State: query, filters, sort key, sort dir, selected IDs.
  • Visible rows are derived.
  • Headers are buttons with sort labels.
  • Empty state explains active filters.
Twist

Add server pagination and explain what moves from client to server.

45 minAsyncUpload

File Upload Queue

Build a queue with selected files, progress, cancel, retry failed, and remove completed.

Acceptance criteria
  • Each file has ID, status, progress, error.
  • Cancel aborts active upload.
  • Retry resets failed status.
  • Input has label and helper text.
Twist

Limit concurrency to two active uploads.

45 minReactKeyboard

Command Palette

Build a command palette with open shortcut, search, grouped results, arrow keys, Enter, Escape, and no-result state.

Acceptance criteria
  • Open with button and keyboard shortcut.
  • Search filters derived results.
  • Active index clamps to bounds.
  • Escape closes and restores focus.
Twist

Include async commands from server and local commands together.

50 minReactTree

File Explorer Tree

Build nested folders with expand/collapse, selected file, create folder, rename, and delete.

Acceptance criteria
  • Expanded IDs are state.
  • Selected ID is state.
  • Tree renders recursively.
  • Actions update immutable data.
Twist

Lazy-load folder children on first expand.

60 minChatAsync

Mini Chat App

Build message list, composer, optimistic send, failed retry, scroll-to-bottom, and streaming placeholder.

High-yield focus

Separate draft, messages, send status, stream status, and retryable failures.

Acceptance criteria
  • User messages append immediately.
  • Failures are visible and retryable.
  • Input remains responsive while sending.
  • New messages scroll into view.
Twist

Responses stream chunks and can be stopped mid-generation.

45 minSystem DesignFeed

Design A Realtime Feed

Design home feed loading, pagination, optimistic likes, new-post notification, ranking boundaries, and observability.

Acceptance criteria
  • Cursor pagination explained.
  • Client cache split from feed order.
  • Realtime updates do not shift reading position.
  • Metrics include load latency and mutation failure rate.
45 minSystem DesignRealtime

Design Token Streaming Chat

Design send, stream, stop, retry, reconnect, ordering, markdown rendering, abuse controls, and mobile performance.

Acceptance criteria
  • SSE vs WebSocket tradeoff stated.
  • Draft persistence covered.
  • Chunk rendering is batched.
  • Retries and cancellation are clear.

08 / study plans

Timeboxed Study Plans

Use the short plan when the interview is soon. Use the longer plan when you want to build deep fluency instead of just warming up.

DailyOne build, one async pattern, one out-loud explanation.
MockRecord whether you shipped, explained, and handled a twist.
ReviewFix one weakness immediately after the mock while memory is fresh.
StopDo not binge passive reading after you can build the pattern once.
Same-day crash plan: 4 hours
High-yield rule

Use this when time is short: type patterns from memory, say the state model out loud, and skip passive review until after the mock.

Hour 1: React state reps

Build tabs, controlled search, derived filtering, and a small form. Say C-S-A-R-E out loud each time.

Hour 2: Async reps

Build fetch with loading/error/empty, AbortController, latest request guard, and debounced search.

Hour 3: Product drill

Build mini chat or command palette. Add keyboard and edge states after the happy path works.

Hour 4: System design + scripts

Talk through chat streaming and realtime feed design. Practice the answer scripts below.

Seven-day ramp
High-yield rule

Every day should produce a visible widget or spoken design answer. Reading only counts if it fixes a miss from a mock.

React basics under pressure

Components, props, state, lists, forms, derived values, no unnecessary effects.

Async and race conditions

Fetch hook, cancellation, debouncing, retries, stale response guards, loading states.

Accessibility and CSS

Modal, tabs, command palette, semantic markup, grid/flex, responsive constraints.

Data-heavy UI

Sortable table, filters, pagination, virtualization discussion, URL state.

Chat and realtime

Optimistic messages, streaming, stop generation, reconnect, observability.

Frontend system design

Design chat, feed, dashboard, notifications. Practice tradeoffs and failure modes.

Mock day

One 45-minute live build and one 45-minute system design answer. Review gaps only.

Thirty-day 0 to hero path
High-yield rule

Depth comes from variations: rebuild the same pattern with local data, server data, realtime updates, and a11y constraints.

  1. Week 1: React fundamentals and state. Build 10 small components from memory.
  2. Week 2: Async, forms, keyboard UX, accessibility. Build 6 product-like widgets.
  3. Week 3: Performance, testing, routing, data caching, offline basics. Build a small dashboard.
  4. Week 4: Frontend system design. Run 8 mocks: chat, feed, autocomplete, dashboard, upload, notifications, docs editor, analytics.

Self-score after every mock

Area012
Clarified scopeStarted coding blindAsked broad questionsAsked implementation-changing questions
State modelDuplicated derived stateMostly clearMinimal source of truth
Working sliceIncompleteHappy path worksHappy path plus edge states
AsyncRace bugs likelyBasic loading/errorCancellation and stale guard
A11yDiv soupSome labels/buttonsKeyboard, labels, status, focus
ExplanationSilent or scatteredSome tradeoffsClear reasoning while coding

09 / out-loud scripts

Short Explanations To Practice

These are not meant to sound rehearsed. They are meant to make your real thinking fast and legible while you code.

Before code"I will name state first, then ship the happy path."
During code"This is derived, so I am not storing it."
Async"Abort handles cleanup; request ID handles response ordering."
End"Next I would test keyboard, async races, mobile, and error states."
Starting"I will first clarify data shape and edge states, then build the happy path, then harden async and accessibility."
State"The source of truth is the query, selected ID, and server result status. The filtered list and selected item are derived during render."
Effect"This effect synchronizes with the network. It cleans up with AbortController and ignores stale responses so older requests cannot overwrite newer UI."
Forms"Submit logic belongs in the submit handler because that is where I know the user action happened."
A11y"I am using real buttons and labels so keyboard and assistive-tech users get the expected behavior for free."
Perf"I would measure before memoizing. The first wins are reducing unnecessary state, avoiding huge renders, and virtualizing long lists."
Design"For v1 I would keep transport simple, define stable IDs, make retries visible, and add observability around latency and failure rates."
Close"Given more time, I would add tests around keyboard behavior, async races, empty/error states, and mobile layout."

Common traps

  • Using useEffect to derive filtered data.
  • Forgetting response.ok with fetch.
  • Letting stale requests overwrite current results.
  • Using array index as key for reorderable lists.
  • Building custom clickable divs instead of buttons.
  • Ignoring loading, empty, and error states until the end.
  • Memoizing everything without proving cost.

Fast debug checklist

  • Can I reproduce the bug with a tiny sequence of clicks?
  • Which state value is wrong?
  • Is it source state or derived state?
  • Is a closure using old state?
  • Did an effect run more often than expected?
  • Is cleanup missing?
  • Is the key preserving or resetting state correctly?

10 / source links

Official References Worth Reviewing

This guide is written as practical prep, with links to official docs for deeper review. Use these references when you want the primary-source version of a concept after practicing the implementation.

React: Thinking in React

Official guide for breaking UI into components, identifying state, and building from a data model.

Open React docs

React: useState

State initialization, updater functions, caveats, object/array state, and state reset patterns.

Open React docs

React: useEffect

Effect synchronization, cleanup, fetch examples, and race-condition notes.

Open React docs

React: You Might Not Need an Effect

Official guidance for deriving render data without unnecessary effects.

Open React docs

React with TypeScript

Event typing, children, style props, and TypeScript learning links.

Open React docs

MDN Fetch API

Fetch, Request, Response, promise behavior, and HTTP error handling context.

Open MDN

MDN AbortController

Abort signals for fetch requests and other async operations.

Open MDN

MDN Accessibility: HTML

Semantic HTML and accessibility basics for forms, controls, text, and structure.

Open MDN

MDN CORS

Cross-origin request rules, browser behavior, and server headers.

Open MDN

web.dev Performance

Performance learning path for loading, responsiveness, metrics, and browser behavior.

Open web.dev

11 / highest yield

The Most High-Yield Stuff

This is the cram sheet. If you only review one section before the round, review this: the moves that most often separate a decent frontend answer from a strong practical engineering answer.

Say first"I will clarify data shape, pick minimum state, ship the happy path, then harden."
Build firstControlled input, derived list, loading/error/empty state, semantic controls.
Defend firstAbort stale fetches, guard response ordering, prevent duplicate submits.
Close firstTests for keyboard, async races, edge states, mobile layout, and failure paths.

Minimum state is the whole game

Before JSX, name the source of truth: user inputs, selected IDs, server status/data/error, and small UI state. Derived values stay out of state.

Effects are only for external synchronization

Fetch, timers, subscriptions, focus, scroll, storage, browser listeners. If it is just calculating what to show, do it during render.

Async needs both cleanup and ordering

AbortController stops old work when possible. A latest request ID stops older responses from overwriting newer UI.

HTML quality is not polish

Use buttons for actions, anchors for navigation, labels for inputs, headings in order, visible focus, and status/alert regions for async updates.

Performance answer starts with measurement

Say which cost you are targeting: network, JS bytes, render count, layout shift, main-thread blocking, list size, or image/font loading.

System design must stay user-visible

Every API/cache/realtime choice should map back to latency, reliability, offline behavior, ordering, accessibility, and observable failures.

The 90-second opening script
Memorize this shape

"I will restate the goal, clarify the data and edge states, write the minimum state model, build the happy path, then harden async, a11y, and performance."

  1. Goal: "We need a UI that lets users search/select/send/filter/etc."
  2. Data: "I need stable IDs, labels, status, timestamps, and any disabled/error fields."
  3. State: "I will store only query, selected ID, status/data/error, draft/open/focused index."
  4. Render: "Visible rows, selected item, counts, and disabled labels are derived."
  5. Hardening: "Then I will add loading, empty, error, keyboard, mobile, stale fetch guard, and tests."
Decision table: state, ref, effect, or derived
ThingUseWhy it matters
query, draft, selectedIdStateUser/server can change it independently.
filteredItems, canSubmit, selectedItemDerived render valueDuplicating it creates drift bugs.
DOM node, timer ID, latest request IDRefMutable value that should not cause a render.
Fetch, listener, subscription, focus, scrollEffectReact is syncing with something outside render.
API result stateStatus unionMake loading, success, empty, and error explicit.
The five patterns to type cold
  • Controlled input + derived filtering: no effect, no duplicated filtered state.
  • Fetch hook: status/data/error, response.ok, abort, latest request guard.
  • Debounce: raw input stays immediate; debounced value drives the side effect.
  • Keyboard widget: active index, clamp bounds, Enter choose, Escape close, focus restore.
  • Optimistic mutation: temporary ID, pending status, reconcile on success, rollback/retry on error.
Highest-yield live-coding scratchpad
/*
Live coding loop:
1. Restate goal in one sentence.
2. Define data shape and stable IDs.
3. Write minimum state:
   - input/draft state
   - selected/open/focused UI state
   - server status/data/error state
4. Derive visible UI during render.
5. Add handlers by user intent.
6. Add edge states:
   - loading
   - empty
   - error
   - disabled/pending
7. Add hardening:
   - AbortController
   - latestRequestId
   - keyboard/focus
   - response.ok
   - retry path
8. Close with tests and tradeoffs.
*/
Most common interviewer twists

React build twists

  • "Now make search server-backed."
  • "Older results are overwriting newer ones."
  • "Add keyboard support."
  • "The list has 10,000 rows."
  • "Persist draft after refresh."
  • "Retry failed sends."

Best response shape

  • Move fetch into an effect or hook with cancellation.
  • Add latest-request guard and explicit status.
  • Add active index, focus restore, and real buttons.
  • Virtualize, paginate, or move filtering server-side.
  • Use storage as external sync with safe fallback.
  • Keep failed item visible with retry state.
Last 10 minutes before interview
Do this, then stop

Read the scripts out loud once. Type the scratchpad once. Then trust the loop instead of cramming more facts.

  1. Say the opening script without looking.
  2. Type controlled search from memory.
  3. Say why filtered data is derived, not state.
  4. Say the abort + latest request ID async explanation.
  5. Say the system design skeleton: product, data, API, state, realtime, perf, security, observability.