6: Interview Simulation Components
Timed Ramp-style live coding challenges, each built from a spec with no help. Components are listed in order of attempt. The annotations focus on what a senior engineer would ask about each implementation after the allotted time — the architectural questions that don’t fit in a 50-minute window but distinguish shallow understanding from deep.
1. Dynamic Prop Component (20 min — warmup)
Concepts used: runtime type narrowing, useRef for interval bookkeeping, useEffect conditional setup, unknown over any
A component that branches on what its input prop actually is at runtime. The falsy case renders a live clock with proper cleanup.
any vs unknown — why it matters here. The spec says “render differently based on what the prop is.” That’s precisely the use case for unknown, not any. any disables all type checking downstream — you can call .map() on it without TypeScript objecting, masking the exact error the branch is supposed to prevent. unknown forces you to narrow before use, which is what you’re doing anyway:
import { useEffect, useRef, useState } from 'react'
function DynamicDisplay({ input }: { input?: unknown }) {
const [date, setDate] = useState<Date>(() => new Date())
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
useEffect(() => {
// Only start the clock for the falsy case
if (!input) {
intervalRef.current = setInterval(() => setDate(new Date()), 1000)
return () => {
if (intervalRef.current) clearInterval(intervalRef.current)
}
}
}, [input]) // depend on input — if it changes from falsy to truthy, effect re-runs and cleanup fires
if (!input) {
return (
<time dateTime={date.toISOString()} aria-live="off">
{date.toLocaleTimeString()}
</time>
)
}
if (Array.isArray(input)) {
return (
<ul>
{input.map((item, i) => (
<li key={i}>{String(item)}</li>
))}
</ul>
)
}
return <div>{String(input)}</div>
}The dependency array choice. The original uses [], meaning the effect runs once on mount. If input later changes from falsy to truthy, the clock keeps running — the cleanup never fires because the effect never re-runs. Including input in the dependency array fixes this: when input changes, React runs the cleanup from the previous effect (clearing the interval if it was set) and runs the new effect (which won’t start the interval since input is now truthy).
String(item) for array rendering. The original renders {item} directly, which throws if item is an object — React can’t render plain objects as children. String(item) is a safe fallback for demo purposes; in production, you’d have a more specific type constraint.
Live demo:
4/29/2026, 3:39:24 PM
2. Master-Detail Job Board (45 min — medium)
Concepts used: discriminated union async state, AbortController, master-detail selection, keyboard navigation, component interface typing
A job board with search-filtered list and detail panel. The pattern appears constantly — file explorers, email clients, settings panels. The interesting engineering decisions are in the selection model and keyboard behavior.
State shape. The naive version uses four separate variables for async state plus highlighted. The corrected version uses a discriminated union and co-locates the jobs data inside it:
import { useState, useEffect, useCallback } from 'react'
interface Job {
id: number
title: string
body: string
}
type JobsState =
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'success'; jobs: Job[] }
interface JobTableProps {
jobs: Job[]
selectedId: number | null
onSelect: (job: Job) => void
}
interface JobDetailProps {
job: Job | null
}
function JobTable({ jobs, selectedId, onSelect }: JobTableProps) {
return (
<ul role="listbox" aria-label="Job listings" style={{ listStyle: 'none', padding: 0 }}>
{jobs.map(job => (
<li
key={job.id}
role="option"
aria-selected={selectedId === job.id}
tabIndex={0}
onClick={() => onSelect(job)}
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') onSelect(job) }}
style={{
padding: '0.5rem',
cursor: 'pointer',
backgroundColor: selectedId === job.id ? '#e5e7eb' : 'transparent',
}}
>
<span style={{ fontSize: 13, fontWeight: selectedId === job.id ? 600 : 400 }}>
{job.title}
</span>
</li>
))}
</ul>
)
}
function JobDetail({ job }: JobDetailProps) {
if (!job) return <p style={{ opacity: 0.5 }}>Select a listing to view details.</p>
return (
<article aria-label={`Job detail: ${job.title}`}>
<h3>{job.title}</h3>
<p>{job.body}</p>
</article>
)
}
export default function JobBoard() {
const [state, setState] = useState<JobsState>({ status: 'loading' })
const [query, setQuery] = useState('')
const [selected, setSelected] = useState<Job | null>(null)
useEffect(() => {
const controller = new AbortController()
fetch('https://jsonplaceholder.typicode.com/posts?_limit=20', {
signal: controller.signal,
})
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json() as Promise<Job[]>
})
.then(jobs => setState({ status: 'success', jobs }))
.catch(err => {
if (err.name === 'AbortError') return
setState({ status: 'error', message: err.message })
})
return () => controller.abort()
}, [])
if (state.status === 'loading') return <p role="status">Loading jobs...</p>
if (state.status === 'error') return <p role="alert" style={{ color: 'red' }}>{state.message}</p>
const filtered = state.jobs.filter(job =>
job.title.toLowerCase().includes(query.toLowerCase())
)
return (
<div style={{ display: 'flex', gap: 30 }}>
<div style={{ flex: '0 0 240px' }}>
<label htmlFor="job-search" style={{ display: 'block', marginBottom: 4 }}>
Search listings
</label>
<input
id="job-search"
type="search"
placeholder="Filter by title..."
value={query}
onChange={e => setQuery(e.target.value)}
style={{ width: '100%', marginBottom: 8 }}
/>
{filtered.length === 0
? <p style={{ opacity: 0.5 }}>No listings match "{query}".</p>
: <JobTable jobs={filtered} selectedId={selected?.id ?? null} onSelect={setSelected} />
}
</div>
<div style={{ flex: 1 }}>
<JobDetail job={selected} />
</div>
</div>
)
}role="listbox" + role="option" + aria-selected. A list of selectable items is semantically a listbox. Without ARIA roles, keyboard users navigating the page with a screen reader hear a generic list with no indication that items are selectable or which is active. The tabIndex={0} + onKeyDown combination makes each item keyboard-accessible — both Enter and Space should activate a selection (matching native <option> behavior).
selectedId over selectedJob. Passing the full job object as the selection marker (highlighted: Job | null) works but creates subtle equality bugs — two Job objects with identical fields are not === equal. Passing selectedId: number | null and comparing with === is reliable and avoids accidental reference inequality after re-fetches.
Live demo:
Loading
3. Day Calendar (50 min — hard, WIP)
Concepts used: interval overlap detection, conditional rendering for multi-hour event spans, form with controlled select
A single-day calendar where clicking an hour slot opens a scheduling form. Events span multiple rows based on duration.
The overlap detection algorithm. withinEventDuration checks whether a given hour falls within any saved event’s range. The core predicate is a half-open interval check: [startHour, startHour + duration). Getting the boundary conditions wrong (using <= instead of < for the end) causes off-by-one bugs where a 1-hour event appears to occupy two slots:
import { useState } from 'react'
interface CalendarEvent {
title: string
duration: number // in hours
startHour: number
}
type FormState = Pick<CalendarEvent, 'title' | 'duration'>
const HOURS = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
const DURATION_OPTIONS = [1, 2, 3, 4] as const
export default function DayCalendar() {
const [events, setEvents] = useState<CalendarEvent[]>([])
const [openAtHour, setOpenAtHour] = useState<number | null>(null)
const [form, setForm] = useState<FormState>({ title: '', duration: 1 })
// Half-open interval: [start, start + duration)
const eventAt = (hour: number): CalendarEvent | undefined =>
events.find(e => hour >= e.startHour && hour < e.startHour + e.duration)
const isOccupied = (hour: number): boolean => !!eventAt(hour)
const handleSlotClick = (hour: number) => {
if (isOccupied(hour)) return // block clicks on occupied slots
setOpenAtHour(hour)
setForm({ title: '', duration: 1 })
}
const handleSave = () => {
if (!openAtHour || !form.title.trim()) return
// Overlap prevention: check that no hour in [start, start+duration) is occupied
const conflict = Array.from(
{ length: form.duration },
(_, i) => openAtHour + i
).some(isOccupied)
if (conflict) {
alert('This time slot overlaps an existing event.')
return
}
setEvents(prev => [...prev, { ...form, startHour: openAtHour }])
setOpenAtHour(null)
}
return (
<div style={{ display: 'flex', gap: 20 }}>
<div style={{ flex: '0 0 200px' }}>
{HOURS.map(hour => {
const event = eventAt(hour)
const isStart = event?.startHour === hour
const occupied = !!event
return (
<div
key={hour}
onClick={() => handleSlotClick(hour)}
style={{
height: 40,
borderTop: '1px solid #e5e7eb',
borderBottom: occupied && !isStart ? '1px solid transparent' : '1px solid #e5e7eb',
backgroundColor: occupied ? '#dbeafe' : 'white',
cursor: occupied ? 'default' : 'pointer',
padding: '0 8px',
display: 'flex',
alignItems: 'center',
fontSize: 13,
}}
role="button"
tabIndex={occupied ? -1 : 0}
aria-label={occupied ? `${hour}:00 - ${event!.title}` : `${hour}:00 - empty slot`}
onKeyDown={e => { if (e.key === 'Enter') handleSlotClick(hour) }}
>
{isStart ? event!.title : hour + ':00'}
</div>
)
})}
</div>
{openAtHour !== null && (
<form
onSubmit={e => { e.preventDefault(); handleSave() }}
style={{ display: 'flex', flexDirection: 'column', gap: 8 }}
aria-label={`New event at ${openAtHour}:00`}
>
<p style={{ margin: 0, fontWeight: 600 }}>New event at {openAtHour}:00</p>
<label htmlFor="event-title">Title</label>
<input
id="event-title"
value={form.title}
onChange={e => setForm(prev => ({ ...prev, title: e.target.value }))}
required
/>
<label htmlFor="event-duration">Duration</label>
<select
id="event-duration"
value={form.duration}
onChange={e => setForm(prev => ({ ...prev, duration: Number(e.target.value) }))}
>
{DURATION_OPTIONS.map(d => (
<option key={d} value={d}>{d} hour{d > 1 ? 's' : ''}</option>
))}
</select>
<div style={{ display: 'flex', gap: 8 }}>
<button type="submit">Save</button>
<button type="button" onClick={() => setOpenAtHour(null)}>Cancel</button>
</div>
</form>
)}
</div>
)
}What’s still missing. Delete: each event needs a remove button accessible from the detail area. True overlap prevention for multi-hour events requires checking the entire proposed span, not just the clicked slot — the implementation above does this with Array.from({length: duration}, ...).some(isOccupied), but the form doesn’t yet visually indicate which durations are available. A production implementation would disable duration options that would create conflicts.
Live demo:
8 AM
9 AM
10 AM
11 AM
12 PM
1 PM
2 PM
3 PM
4 PM
5 PM
6 PM
Please select a time in the Calendar to schedule an event
4. Mini Spreadsheet (50 min — hard)
Concepts used: 2D array state, editable cell with useRef auto-focus, formula parsing, the store-raw-resolve-at-render architecture
A 5×5 spreadsheet supporting =SUM(A1:A3) formulas. The most interesting part isn’t the formula parser — it’s the architectural choice of when to evaluate formulas.
Store-raw vs store-evaluated. The naive approach computes the formula result at save time and stores it. This breaks dependent cells: if A3 contains =SUM(A1:A2) and you later edit A1, the stored value in A3 doesn’t update. The correct architecture stores raw input (the string '=SUM(A1:A2)') and evaluates at render time:
import { useState, useRef, useEffect } from 'react'
const COLS = Array.from({ length: 5 }, (_, i) => String.fromCharCode(65 + i)) // ['A'..'E']
const ROWS = Array.from({ length: 5 }, (_, i) => i + 1) // [1..5]
type Grid = string[][] // raw input; formulas stored as '=SUM(A1:A3)'
function cellId(col: string, row: number) { return `${col}${row}` }
function parseRef(ref: string): [string, number] {
return [ref[0], parseInt(ref.slice(1), 10)]
}
// Resolve a cell to its numeric value, following formula references
function resolve(grid: Grid, col: string, row: number): number {
const colIdx = col.charCodeAt(0) - 65
const rowIdx = row - 1
const raw = grid[colIdx]?.[rowIdx] ?? ''
if (!raw.startsWith('=SUM(')) return Number(raw) || 0
// Parse =SUM(A1:A3) → sum rows 1-3 of column A
const match = raw.match(/^=SUM\(([A-E])(\d):([A-E])(\d)\)$/)
if (!match) return 0
const [, startCol, startRow, endCol, endRow] = match
if (startCol !== endCol) return 0 // cross-column SUM not supported
let sum = 0
for (let r = parseInt(startRow, 10); r <= parseInt(endRow, 10); r++) {
sum += resolve(grid, startCol, r)
}
return sum
}
interface CellProps {
value: string // raw input
display: number | string // resolved display value
location: string
onCommit: (location: string, value: string) => void
}
function Cell({ value, display, location, onCommit }: CellProps) {
const [isEditing, setIsEditing] = useState(false)
const [draft, setDraft] = useState(value)
const inputRef = useRef<HTMLInputElement>(null)
// Sync draft when external value changes (e.g. parent undo)
useEffect(() => {
if (!isEditing) setDraft(value)
}, [value, isEditing])
// Auto-focus on edit mode entry
useEffect(() => {
if (isEditing) inputRef.current?.focus()
}, [isEditing])
const commit = () => {
onCommit(location, draft)
setIsEditing(false)
}
if (isEditing) {
return (
<input
ref={inputRef}
value={draft}
onChange={e => setDraft(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') commit()
if (e.key === 'Escape') { setDraft(value); setIsEditing(false) }
}}
onBlur={commit}
style={{ width: '100%', boxSizing: 'border-box' }}
aria-label={`Edit cell ${location}`}
/>
)
}
return (
<div
role="button"
tabIndex={0}
onClick={() => setIsEditing(true)}
onKeyDown={e => { if (e.key === 'Enter' || e.key === 'F2') setIsEditing(true) }}
aria-label={`Cell ${location}: ${display}`}
style={{ minHeight: 28, padding: '2px 4px', cursor: 'text' }}
>
{display}
</div>
)
}
export default function Spreadsheet() {
// 2D array: grid[colIdx][rowIdx] = raw string
const [grid, setGrid] = useState<Grid>(() =>
COLS.map(() => ROWS.map(() => ''))
)
const handleCommit = (location: string, raw: string) => {
const [col, row] = parseRef(location)
const colIdx = col.charCodeAt(0) - 65
const rowIdx = row - 1
setGrid(prev => prev.map((c, ci) =>
ci === colIdx
? c.map((r, ri) => ri === rowIdx ? raw : r)
: c
))
}
return (
<div style={{ overflowX: 'auto' }}>
<table style={{ borderCollapse: 'collapse' }}>
<thead>
<tr>
<th />
{COLS.map(col => <th key={col} style={{ padding: '4px 8px', fontWeight: 600 }}>{col}</th>)}
</tr>
</thead>
<tbody>
{ROWS.map(row => (
<tr key={row}>
<th scope="row" style={{ padding: '4px 8px', fontWeight: 600 }}>{row}</th>
{COLS.map(col => {
const colIdx = col.charCodeAt(0) - 65
const raw = grid[colIdx][row - 1]
const display = raw.startsWith('=') ? resolve(grid, col, row) : (raw || '')
return (
<td key={col} style={{ border: '1px solid #e5e7eb', minWidth: 80, padding: 0 }}>
<Cell
value={raw}
display={display}
location={cellId(col, row)}
onCommit={handleCommit}
/>
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
)
}Why the resolve-at-render architecture is correct. When A1 changes, React re-renders the entire grid. Every cell that displays a formula calls resolve() again with the new grid state — so A3’s =SUM(A1:A2) automatically picks up the new A1 value. No manual dependency tracking, no stale cached values. The tradeoff is that resolve() runs on every render for every formula cell. For a 5×5 grid this is negligible; for a large grid with complex formulas, you’d want useMemo keyed on the cell’s dependencies, or a topological evaluation order that short-circuits unchanged cells.
F2 to enter edit mode. This is the standard spreadsheet keybinding. Small details like this show familiarity with the domain being built.
Live demo:
5. Transaction Ledger (35 min — intern level)
Concepts used: form with controlled inputs, validation with early return, reduce for running total, filter dropdown, delete, typed interfaces
A financial transaction tracker. The interesting decisions are in the validation architecture and derived total.
Interface naming and type discipline:
import { useState } from 'react'
// Capitalize interface names — TypeScript convention
type TransactionType = 'Income' | 'Expense'
interface Transaction {
id: string // crypto.randomUUID(), not incrementing number
description: string
amount: number // always positive; sign is determined by type
type: TransactionType
}
type FilterCategory = 'All' | TransactionType
interface TransactionFormState {
description: string
amount: string // string while editing, parsed to number on save
type: TransactionType
}amount is stored as a string in form state because <input type="number"> values are always strings until you parse them. Storing number in form state and binding it to the input creates a controlled/uncontrolled mismatch when the field is empty (empty string is not a valid number).
Validation and functional updates:
const INITIAL_FORM: TransactionFormState = { description: '', amount: '', type: 'Income' }
export default function TransactionLedger() {
const [transactions, setTransactions] = useState<Transaction[]>([])
const [filter, setFilter] = useState<FilterCategory>('All')
const [form, setForm] = useState<TransactionFormState>(INITIAL_FORM)
const [error, setError] = useState('')
const handleSave = () => {
if (!form.description.trim()) { setError('Description is required'); return }
const amount = Number(form.amount)
if (!form.amount || isNaN(amount) || amount <= 0) { setError('Amount must be a positive number'); return }
setTransactions(prev => [...prev, {
id: crypto.randomUUID(),
description: form.description.trim(),
amount,
type: form.type,
}])
setForm(INITIAL_FORM)
setError('')
}
const handleDelete = (id: string) => {
setTransactions(prev => prev.filter(t => t.id !== id))
}
// Derived state — no separate useState needed, guaranteed in sync
const balance = transactions.reduce(
(sum, t) => t.type === 'Income' ? sum + t.amount : sum - t.amount,
0
)
const visible = filter === 'All'
? transactions
: transactions.filter(t => t.type === filter)
return (
<div>
<div style={{ marginBottom: 16 }}>
<input
placeholder="Description"
value={form.description}
onChange={e => setForm(prev => ({ ...prev, description: e.target.value }))}
aria-label="Transaction description"
/>
<input
type="number"
placeholder="Amount"
min="0.01"
step="0.01"
value={form.amount}
onChange={e => setForm(prev => ({ ...prev, amount: e.target.value }))}
aria-label="Transaction amount"
/>
<select
value={form.type}
onChange={e => setForm(prev => ({ ...prev, type: e.target.value as TransactionType }))}
aria-label="Transaction type"
>
<option value="Income">Income</option>
<option value="Expense">Expense</option>
</select>
<button onClick={handleSave}>Add</button>
{error && <p role="alert" style={{ color: 'red', margin: '4px 0 0' }}>{error}</p>}
</div>
<div style={{ marginBottom: 12 }}>
<strong>Balance: </strong>
<span style={{ color: balance >= 0 ? 'green' : 'red' }}>
{balance >= 0 ? '+' : ''}{balance.toFixed(2)}
</span>
{' '}
<select
value={filter}
onChange={e => setFilter(e.target.value as FilterCategory)}
aria-label="Filter transactions"
>
<option value="All">All</option>
<option value="Income">Income</option>
<option value="Expense">Expense</option>
</select>
</div>
{visible.length === 0
? <p style={{ opacity: 0.5 }}>No transactions{filter !== 'All' ? ` (${filter})` : ''}.</p>
: (
<ul style={{ listStyle: 'none', padding: 0 }}>
{visible.map(t => (
<li key={t.id} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ flex: 1 }}>{t.description}</span>
<span style={{ color: t.type === 'Income' ? 'green' : 'red', minWidth: 80, textAlign: 'right' }}>
{t.type === 'Income' ? '+' : '-'}{t.amount.toFixed(2)}
</span>
<button
onClick={() => handleDelete(t.id)}
aria-label={`Delete transaction: ${t.description}`}
style={{ fontSize: 11 }}
>
Remove
</button>
</li>
))}
</ul>
)
}
</div>
)
}=== '' not == '' and !== id not != id. Loose equality (==) coerces types before comparing — '' == false is true in JavaScript. In a codebase with TypeScript’s strict mode enabled, loose equality often triggers a lint warning. The consistent habit is === everywhere unless coercion is intentional and documented.
amount as string in form, number in the domain model. This boundary is where most form validation bugs live. The form owns a string representation; the domain owns a number. The conversion (Number(form.amount)) with explicit validation (isNaN, > 0) happens at the boundary — the save handler. Don’t let the string/number ambiguity leak into the Transaction interface.
Live demo:
Error:
Total: 0
Filter by Category
| Description | Amount |
|---|