6: Interview Simulation Components
Timed Ramp-style live coding challenges. Each one was built from a spec with no help, simulating a 50-minute interview screen. Components are listed in the order they were attempted.
1. Dynamic Prop Component (20 min — warmup)
Concepts used: useEffect cleanup, useRef, Array.isArray(), conditional rendering, runtime type checking
A component that renders differently based on what type its input prop is. The falsy case renders a live clock that cleans up its interval on unmount.
import { useEffect, useRef, useState } from "react"
function DynamicDisplay({input} : {input?: any}) {
const [date, setDate] = useState(new Date())
const intervalId = useRef(0)
useEffect(() => {
if (!input) {
intervalId.current = window.setInterval(() => setDate(new Date()), 1000)
return () => window.clearInterval(intervalId.current)
}
}, [])
if (!input) {
return <div><p>{date.toLocaleString()}</p></div>
} else if (Array.isArray(input)) {
return <div>{input.map(item => <div>{item}</div>)}</div>
} else {
return <div>{input}</div>
}
}Live demo:
2/16/2026, 11:43:26 PM
Key takeaways:
- Runtime type checks:
!inputfor falsy,Array.isArray()for arrays, else for everything else.typeofwon’t distinguish arrays from objects. - Conditional cleanup: The
useEffectonly sets up the interval when!input, preventing unnecessary timers for the array/primitive cases.
2. Master-Detail Job Board (45 min — medium)
Concepts used: useEffect + fetch, loading/error states, search filter, master-detail selection, component extraction
A LinkedIn-style job board that fetches listings from an API, filters by search, and shows details for the selected job. Split across 4 files: index.tsx, JobTable.tsx, JobDetail.tsx, interface.ts.
// index.tsx — parent component
import { useState, useEffect } from 'react'
export default function JobBoard() {
const [loading, setLoading] = useState(true)
const [showError, setShowError] = useState(false)
const [draft, setDraft] = useState("")
const [highlighted, setHighlighted] = useState<Job | null>(null)
const [jobs, setJobs] = useState<Job[]>([])
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts?_limit=20")
.then(res => {if (!res.ok) throw new Error("Failed to fetch"); return res.json()})
.then(data => {setLoading(false); setJobs(data)})
.catch(err => {setLoading(false); setShowError(true)})
}, [])
if (loading) return <p>Loading</p>
if (showError) return <p>Failed to Fetch</p>
return (
<div style={{display: "flex", gap: "30px"}}>
<div>
<input placeholder="Search by Job Title" value={draft}
onChange={(e) => setDraft(e.target.value)} />
<JobTable
posts={jobs.filter(job => job.title.toLowerCase().includes(draft.toLowerCase()))}
highlighted={highlighted}
handleSelect={(job) => setHighlighted(job)} />
</div>
<JobDetail job={highlighted} />
</div>
);
}// JobTable.tsx — list with highlight
function JobTable({posts, highlighted, handleSelect}) {
return (
<>
{posts.map(post =>
<div key={post.id} onClick={() => handleSelect(post)}
className={`post ${highlighted?.id === post.id ? "highlighted" : ""}`}>
<h3 style={{fontSize: "13px", margin: 0}}>{post.title}</h3>
</div>
)}
</>
)
}// JobDetail.tsx — detail panel
function JobDetail({job}) {
if (job) {
return <div><h3>{job.title}</h3><p>{job.body}</p></div>
} else {
return <p>Select a Job Listing</p>
}
}Live demo:
Loading
Key takeaways:
- Master-detail pattern: One
highlightedstate (Job | null) drives both the list highlighting and the detail panel content. - Early return for loading/error: Cleaner than nesting ternaries inside the main JSX.
highlighted?.id === post.id: Optional chaining handles the null case without an explicit check.- Component extraction emerged naturally:
JobTablehandles rendering,JobDetailhandles display, parent handles state.
3. Day Calendar (50 min — hard, WIP)
Concepts used: useState, computed values, conditional styling, form with controlled select, event spanning
A single-day calendar where clicking an hour slot opens a form to schedule an event. Events span multiple rows based on duration. Work in progress — saved events render but delete and overlap prevention are not yet implemented.
import { useState } from 'react'
interface Event {
title: string, duration: number, startHour: number
}
export default function DayCalendar() {
const hours = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
const [events, SetEvents] = useState<Event[]>([])
const [openForm, SetOpenForm] = useState(false)
const [newEvent, setNewEvent] = useState<Event>({
title: "untitled event", duration: 1, startHour: 0
})
const [startHour, setStartHour] = useState<number | null>(null)
// Check if any saved event covers this hour
const withinEventDuration = (hour: number) => {
return events.find(event =>
hour >= event.startHour && hour < (event.startHour + event.duration))
}
// Render: hour rows on left, form on right
// Events get gray background + merged borders for multi-hour spans
}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
Key takeaways:
- Event spanning:
withinEventDurationchecks if any saved event’s range covers a given hour — used for both rendering and border merging. - Border merging: Adjacent event slots hide their shared border using conditional
borderTop/borderBottomwithtransparent. - Still needs: Delete functionality, overlap prevention (block clicks on occupied slots), and reading saved events properly.
4. Mini Spreadsheet (50 min — hard)
Concepts used: useState, editable cell toggle, Array.from + String.fromCharCode for grid generation, formula parsing
A 5x5 spreadsheet with editable cells that supports =SUM(A1:A3) formulas. Clicking a cell enters edit mode; pressing Enter saves.
import { useState } from 'react'
export default function Spreadsheet() {
const horizontalIndices = Array.from({length: 5}, (_, i) => String.fromCharCode(65 + i))
const verticalIndices = Array.from({length: 5}, (_, i) => i + 1)
const [data, setData] = useState(horizontalIndices.map(() => verticalIndices.map(() => "")))
const handleEdit = (cell: string, newValue: string) => {
let sum = 0;
if (newValue.startsWith("=SUM(")) {
// Parse =SUM(A1:A3) → extract column, start row, end row
// Reduce over the column's rows to compute the sum
}
// Update the target cell with immutable nested .map()
}
// Grid renders columns as flex row, cells as editable divs
}
function Cell({value, location, handleEdit}) {
const [draft, setDraft] = useState('')
const [isEditing, setIsEditing] = useState(false)
// Click → edit mode, Enter → save, Blur → exit
}Live demo:
Key takeaways:
- Grid generation:
Array.from({length: 5}, (_, i) => String.fromCharCode(65 + i))generates['A', 'B', 'C', 'D', 'E']without hardcoding. - Editable cell pattern:
isEditingboolean toggles between<div>(display) and<input>(edit). Same pattern from the advanced exercises. - Architectural gap: SUM computes at save time and stores the result. A better approach: store the raw formula, resolve at render time so dependent cells auto-update.
5. Transaction Ledger (35 min — intern level)
Concepts used: useState, form validation, derived state (running total), filter dropdown, delete, component extraction
A financial transaction tracker with add/delete, income/expense color coding, running total, and category filtering. Running total uses derived state (.reduce() on render) instead of a separate state variable.
import { useState } from "react"
interface transaction {
description: string, amount: number, type: string, id: number
}
export default function TransactionLedger() {
const [transactions, setTransactions] = useState<transaction[]>([])
const [filterCategory, setfilterCategory] = useState("All")
const [error, setError] = useState("")
const handleSaveTransaction = (newTransaction: transaction) => {
if (newTransaction.description == "") {
setError("Description must not be empty"); return
} else if (newTransaction.amount <= 0) {
setError("Amount must be a positive sum"); return
} else {
setTransactions([...transactions, newTransaction])
}
}
const handleDeleteTransaction = (id: number) => {
setTransactions(transactions.filter(item => item.id != id))
}
// Total derived from state — no separate useState needed
// Total: {transactions.reduce((sum, t) =>
// t.type === "Income" ? sum + Number(t.amount) : sum - Number(t.amount), 0)}
}Live demo:
Error:
Total: 0
Filter by Category
| Description | Amount |
|---|
Key takeaways:
- Derived state for total: Instead of a separate
totaluseState that can drift out of sync, the total is computed fromtransactionson every render using.reduce(). - Validation with early return: Check conditions, set error,
return— clean pattern that avoids deeply nested if/else. e.target.namepattern: OneonChangehandler for all form inputs, keyed by thenameattribute.