Redux with Immer
advancedA mini Redux store with Immer-style draft mutations — reducers, subscriptions, and immutability.
Implement the classnames utility in JavaScript — type dispatch over strings, objects, and nested arrays, the empty-string join bug, and the clsx comparison.
Implement
classNames(...)— the npm utility with ~500M weekly downloads combined withclsx: accept strings, numbers, objects ({ name: condition }), and arbitrarily nested arrays; join the truthy results with single spaces.
classNames("btn", { "btn-active": isActive, "btn-disabled": !enabled }, ["m-2", extra && "shadow"]);It's a beginner-friendly question with real grading criteria: clean type dispatch (the same skill as custom typeof and JSON.stringify, pocket-sized), recursion for nesting, and one specific output bug most implementations ship.
function classNames(...args) {
const classes = [];
for (const arg of args) {
if (!arg) continue; // false, null, undefined, 0, "" — all skipped
if (typeof arg === "string" || typeof arg === "number") {
classes.push(String(arg));
} else if (Array.isArray(arg)) {
const nested = classNames(...arg); // recurse — arrays flatten
if (nested) classes.push(nested); // ← guard: don't push ""
} else if (typeof arg === "object") {
for (const key of Object.keys(arg)) {
if (arg[key]) classes.push(key); // key in, value decides
}
}
// functions, symbols, etc.: silently ignored — matching the real library
}
return classes.join(" ");
}classNames("btn", "btn-primary"); // "btn btn-primary"
classNames("btn", { active: true, disabled: false }); // "btn active"
classNames(["btn", ["nested", { deep: true }]]); // "btn nested deep"
classNames("a", null, undefined, false, 0, "", "b"); // "a b" — one space
classNames({ [`col-${size}`]: true }); // computed keys work
classNames(); // ""The recursive array branch returns a string — and if the whole sub-array was falsy (["x", [false, null]]), that string is "". Push it unguarded and join(" ") happily writes a double space:
brokenClassNames("a", [false], "b"); // "a b" ← two spacesTwo spaces in className is usually harmless to the browser (class parsing splits on whitespace runs) — which is exactly why the bug ships: nothing visibly breaks until a test does exact string comparison, or something downstream splits on " " and gets a phantom "" entry. The one-line guard (if (nested)) fixes it; naming why the bug survives in the wild is the senior touch. (Alternative structure: have the recursion push into a shared array instead of returning strings — no join-of-joins, no bug by construction. Offering the restructure is even better than the guard.)
Object.keys, not for...in — own keys only; a polluted or prototyped object won't leak inherited "classes" (the same discipline as Object.assign).extra && "shadow" works because false/undefined/"" are skipped; 0 being skipped is specified library behavior too (classNames(0) → ""), while classNames(1) → "1". Numbers are in the contract because people do col-${n} variants.clsx — the modern, smaller drop-in (it's what shadcn/ui pairs with tailwind-merge in cn()); same semantics minus some legacy input types. If the interviewer's follow-up is "what does tailwind-merge add?" — conflict resolution (p-2 p-4 → p-4), which is a genuinely different, harder problem (parsing class semantics, not just joining strings). Placing the three tools correctly is 2026 frontend literacy.""-push double space — the bug above.[[["a"]]] → "a"; recursion handles arbitrary depth (it's deep flatten fused with a filter — same skeleton).{ [`theme-${mode}`]: true } must work; it does for free via Object.keys, but interviewers check you know why (the object was built before your function ever saw it).classNames(" a ", "b") → " a b" — the library does not trim; garbage in, garbage out is the documented contract. State it rather than silently "fixing" it.for...in over the object branch.args.filter(Boolean).join(" ") one-liners that ignore objects and arrays entirely — handles a third of the API.type Arg = string | number | null | undefined | false | Record<string, unknown> | Arg[] — a nice small exercise in recursive type aliases.tailwind-merge's core idea." — last-wins per utility group (p-2 p-4 conflict): requires mapping class → group (parse prefixes), then a Map keyed by group — a real parsing/design escalation from string joining.`btn ${isActive ? "active" : ""}` leaves stray spaces and scales miserably past two conditions; the utility exists because the literal version becomes unreadable — a taste answer, and the honest origin story.A mini Redux store with Immer-style draft mutations — reducers, subscriptions, and immutability.
Diff two virtual trees and apply minimal DOM mutations — the reconciliation step that makes the virtual DOM worth having.
Rebuild real DOM from the virtual tree, completing the render pipeline.
Turn real DOM into a plain-object tree — the first half of understanding how React represents UI.