Recommended preparation

Try first

Frame your answer before reading the reference

Junior10 min first pass

Candidate prompt

Design the toast feedback layer for a multi-route web app. After a user saves a profile, any feature or service can show a success message; an edit conflict may show Review or Undo. Focus first on the race where manual dismiss and an expiry timer fire together: the toast and timer must be cleaned up once. Explain where records, rendering, and timers live, then show how a burst, route change, and screen-reader update remain safe.

Constraints

  • Show no more than three toasts.
  • Actions and critical messages persist.
  • Services can trigger global feedback.
  • Rerenders never repeat speech.

Helpful before you start

  • Component state
  • Timeout cleanup
  • ARIA live regions

What good looks like in a 10-minute first pass

Must cover

  • One global owner orders the visible stack and overflow queue.
  • Dismiss and timeout share cleanup; only the first changes state.

Strong signals

  • Actionable messages persist; the viewport owns no timers.
  • Separate announcement identity prevents repeat speech after rerenders.

Stretch if time

Route scope, pause/resume, and measured fair overflow.

Avoid

Each component owns timers or announces again on every render.

Need a hint? Three decisions to make
  1. Choose the owner of global commands, ordered records, and rendering.
  2. Define one timer lifecycle that resolves dismiss-timeout races safely.
  3. Separate visible stacking policy from accessible announcement policy.
Practice this exact case

Guided mock requires a tablet or desktop viewport of at least 768px. It will not start automatically.

Requirements

A 10-minute answer
  1. Open with the user story
    A user saves a profile and sees Profile saved without losing focus. A later edit conflict shows Review or Undo and must remain available until it is resolved or safely dismissed. Either event may come from a component or a service, so callers need one global command API rather than a toast container on every page. This gives the design two concrete records to follow instead of starting with an abstract notification platform.
  2. Set the boundary
    Keep the scope on the client feedback layer. It accepts typed commands, keeps at most three records visible, and puts additional distinct records into an ordered overflow queue. Low-risk status messages may expire. Actionable and critical messages are persistent because a timer must not remove the user’s only recovery path. A server owns the profile save or conflict itself; the toast only reports its result and exposes a command such as Review.
  3. Assign one owner per job
    The store owns normalized records plus the visible and queued order. A lifecycle coordinator owns every expiry handle and all cleanup. The viewport renders the store through one root portal, applies the three-item limit, and sends dismiss or action intent back; it never starts a timer. A separate announcer tracks announcement identity and writes eligible messages to a stable live region once, so visual rerenders do not repeat speech.
  4. Resolve the required race
    Manual dismiss and timeout both request remove(toastId, cause). The first request marks that ID removed, tells the lifecycle coordinator to clear its handle, and lets the store promote the next queued record. The second request reads the terminal state and does nothing. Cleanup is therefore idempotent: there is one removal transition, one timer disposal, and no late callback that can delete a newer toast or mutate an unmounted viewport.
  5. Close with observable behavior
    A burst never renders more than three toasts, but distinct results remain ordered in the queue. The viewport becomes full-width on a narrow screen without changing record identity. Ordinary feedback uses a polite status announcement; genuinely urgent feedback may use an alert. Appearance never steals focus, action controls remain keyboard reachable, and the announcer suppresses a repeated ID even when React rerenders, reorders, or moves its visual element.
Questions for the profile scenario
  • Can services outside the component tree trigger feedback?
  • Which outcomes are safe to expire, and which expose an action?
  • Is three the product’s visible-stack limit on every breakpoint?
  • Which events have a stable key that makes duplicate collapse safe?
Baseline constraints
  • One root viewport renders non-blocking feedback.
  • The ordered overflow queue does not lose a distinct outcome.
  • Persistent actions never inherit a generic expiry.
  • One eligible message produces at most one live-region update.
Client/server ownership
The backend contract owns profile persistence, conflict resolution, and Undo. The toast layer owns command normalization, order, expiry cleanup, rendering, and announcements. We reject feature-owned containers because they can remove a user’s visible recovery action when the feature unmounts.

Architecture

Ownership map
OwnerKeepsDoes not own
Toast storeNormalized records, visible order, overflow order, and terminal removal state.Browser timeout handles or DOM nodes.
Lifecycle coordinatorOne expiry handle per timed record and idempotent disposal by ID.Visual stacking, focus, or live-region text.
Toast viewportA portal view of at most three records plus keyboard-operable actions.Independent timers, copied records, or announcement history.
AnnouncerA stable live region and a set of announcement IDs already emitted.Toast lifetime or visual placement.
Canonical model
toast.success(...) creates a command. The store assigns an ID and places its record in the visible stack or ordered queue. The lifecycle coordinator schedules expiry only when the normalized lifetime is timed. The viewport renders store state and returns user intent. The announcer receives an eligible announcement event once; it does not infer new speech from every render.
Profile-save flow
  1. Accept the command
    A feature or service calls toast.success('Profile saved', { dedupeKey: 'profile-save:42' }). The API validates the options and sends an add command to the store.
  2. Admit the record
    The store assigns a stable ID. If fewer than three records are visible it appends the record there; otherwise it appends the distinct record to the ordered queue.
  3. Schedule and render
    The lifecycle coordinator schedules the expiry for this timed status. The viewport renders the same record through the root portal, while the announcer publishes Profile saved only if that ID has not been announced.
  4. Converge removal
    Dismiss and timeout submit the same ID. The store accepts the first transition, the coordinator clears the handle, and a queued record may become visible. Any late signal is a no-op.
Worked example: profile save and edit conflict
EventState changeUser-visible result
Profile save succeedsInsert one polite, timed status record.Profile saved appears without moving focus.
The same success event repeatsMatch its safe dedupe key instead of inserting another record.The sentence is not duplicated visually or audibly.
An edit conflict arrivesInsert a persistent action record with a Review command.Review remains available until resolution or safe dismissal.
Dismiss races with expiryOnly the first remove transition changes store state; cleanup runs once.The toast disappears once and the next queued record is stable.
Flow from a global toast command through the store, lifecycle coordinator, viewport, and one-time live-region announcer.
Command and lifecycle ownership: rendering never owns timers or announcement identity.
Read diagram as text

Text fallback: A global command enters the toast store. The lifecycle coordinator schedules or cancels expiry, the viewport renders at most three records, and the announcer speaks each eligible toast ID once.

Data

Keep store records separate from browser runtime and announcement history. The store can then reconcile commands without serializing timeout handles or tying speech to a rendered row.
type ToastVariant = 'success' | 'error' | 'warning' | 'info';
type ToastPlacement = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
type ToastLifetime =
  | { kind: 'persistent' }
  | { kind: 'timed'; durationMs: number };
type ToastActionState = 'idle' | 'pending' | 'failed';

interface ToastRecord {
  id: string;
  dedupeKey?: string;
  variant: ToastVariant;
  message: string;
  description?: string;
  placement: ToastPlacement;
  lifetime: ToastLifetime;
  urgency: 'polite' | 'urgent';
  action?: {
    label: string;
    commandId: string;
    state: ToastActionState;
  };
}

interface ToastRuntime {
  toastId: string;
  deadline?: number;
  timerHandle?: ReturnType<typeof setTimeout>;
  status: 'scheduled' | 'removing' | 'disposed';
}

interface AnnouncementState {
  emittedIds: Set<string>;
}

interface ToastState {
  visible: ToastRecord[];
  queued: ToastRecord[];
  defaultPlacement: ToastPlacement;
  defaultTimedDurationMs: number;
  maxVisible: number;
}
State transitions
  1. Add
    Normalize the command into a ToastRecord with a unique ID. Put it in visible when capacity remains; otherwise append the distinct record to queued. A safe dedupe key may update an existing matching record rather than create a copy.
  2. Schedule
    For a timed lifetime, the lifecycle coordinator creates one ToastRuntime keyed by toastId. Persistent records have no expiry handle, so Review or Undo cannot disappear because a provider default was applied accidentally.
  3. Remove
    The first dismiss or timeout marks runtime removing, clears its handle, removes the record, and promotes the next queued item. A later request sees removing, disposed, or a missing ID and returns without another state change.
  4. Announce
    The announcer checks emittedIds before updating its live region. It records identity outside ToastRecord, so a visual reorder, responsive move, or component rerender cannot make the same message sound new.
Identity serves different jobs
id identifies one toast lifecycle, dedupeKey joins repeated reports of the same outcome, commandId identifies an action, and emittedIds prevents repeat speech. Reusing one field for all four jobs makes race behavior ambiguous.
State ownership
StateOwnerReason
visible and queuedToast storeOne ordered source drives admission and promotion.
ToastRuntime by toastIdLifecycle coordinatorBrowser handles are created and disposed in one place.
Rendered itemsToast viewportThe UI derives rows from store records instead of copying them.
emittedIdsAnnouncerAnnouncement identity remains independent of the DOM.

Interfaces

Expose semantic choices to feature code and keep ownership mechanics private. A caller describes the feedback; it does not select an ARIA role, create a timeout, or manipulate the portal.
type ToastLifetimeInput =
  | { kind: 'persistent' }
  | { kind: 'timed'; durationMs?: number };

interface ToastOptions {
  description?: string;
  placement?: ToastPlacement;
  lifetime?: ToastLifetimeInput;
  urgency?: 'polite' | 'urgent';
  dedupeKey?: string;
  action?: {
    label: string;
    command: () => Promise<void> | void;
  };
}

interface ToastPatch extends Partial<ToastOptions> {
  message?: string;
}

interface ToastApi {
  success(message: string, options?: ToastOptions): string;
  error(message: string, options?: ToastOptions): string;
  warning(message: string, options?: ToastOptions): string;
  info(message: string, options?: ToastOptions): string;
  update(id: string, patch: ToastPatch): void;
  dismiss(id: string): void;
  dismissAll(): void;
}

interface ToastProviderProps {
  children: unknown;
  placement?: ToastPlacement;
  timedDurationMs?: number;
  maxVisible?: number;
}
Command path
  1. Describe the outcome
    Feature code calls toast.success('Profile saved', { dedupeKey: 'profile-save:42', lifetime: { kind: 'timed' } }). For a conflict it supplies a Review action and persistent lifetime.
  2. Normalize once
    The API resolves provider defaults, validates that an action cannot inherit timed expiry, registers the action command, and sends one add command with a stable toast ID to the store.
  3. Render derived state
    The viewport subscribes to visible records and renders buttons whose handlers send action or dismiss intent. It never copies records into local component state and never calls setTimeout.
  4. Remove through one gate
    Both dismiss(id) and the lifecycle coordinator’s expiry signal reach the same remove operation. The accepted transition disposes runtime before publishing the new store snapshot; a second signal for that ID returns unchanged.
Contract decisions
DecisionWhy it is visible in the API
Return the toast IDThe caller can update progress or dismiss one known result without searching by message text.
Use lifetime instead of raw milliseconds alonePersistent is an explicit semantic state, not a special duration value.
Accept urgency, not ARIA roleThe announcer maps ordinary status to polite speech and reserves interruption for urgent outcomes.
Keep maxVisible on the providerThe application configures one viewport policy instead of allowing each caller to compete for space.
Action failure
While Review or Undo runs, update the same toast to pending and disable repeat activation. On failure, keep the persistent record visible with retry text. On success, update or dismiss it through the same ID; the domain operation, not the toast, decides whether recovery succeeded.

Optimizations

Harden the lifecycle before adding policy. Verify bounded bursts, recoverable actions, one cleanup, and one announcement per eligible ID.
Failure handling
FailureResponseInvariant
Manual dismiss and expiry fire togetherSend both signals through remove(id); accept the first transition and ignore the late one.Store removal and timeout disposal each happen once.
A burst exceeds the visible limitRender three records and append additional distinct records to the ordered queue.The viewport stays bounded without losing an outcome.
The same save event repeatsCollapse only records that share a safe dedupe key.Unrelated messages and recovery actions remain distinct.
Review action failsKeep the persistent toast, expose retry, and retain failure context.The user’s recovery path remains available.

Accessibility and responsive behavior

Use a stable status live region with polite delivery for ordinary feedback and reserve alert for real interruption. Appearance never moves focus. Review, Undo, retry, and close controls have descriptive labels, visible focus treatment, and normal keyboard order. A narrow viewport uses available width without changing record identity. Decorative motion respects reduced-motion preferences.

Verification and measurement

Tests that protect the design
  • Fire dismiss and expiry in both orders; assert one removal and one disposal.
  • Send four distinct commands; assert three visible and one queued record.
  • Rerender and reorder a record; assert its announcement ID is emitted once.
  • Fail an action; assert the persistent toast keeps keyboard-operable retry.
  • Use long localized text; assert wrapping does not cover controls.
Instrument queue depth, duplicate collapse, removal cause, action outcomes, and announcement emission. Review by viewport class and assistive-technology test runs before changing capacity. Use an inline or page error when recovery needs more context.
Expert stretch
Cover route scope, pause and resume, and measured policies for fair overflow. Define which records survive navigation. If research shows users need more reading time, resume from stored remaining lifetime. If telemetry shows starvation, compare admission using observed urgency, age, and recovery value instead of an invented priority rule.
Timeline showing manual dismiss and timeout racing into one idempotent remove transition with timer cleanup.
Dismiss-timeout race: either signal may arrive first, but removal and cleanup happen once.
Read diagram as text

Text fallback: Manual dismiss marks the toast removing and clears its timeout. A late timeout observes the terminal state and does nothing; if timeout wins, a later dismiss is also a no-op.

Use the Question Library for baseline coverage, then move into a Study Plan before targeted Company Prep.