classnames()
beginnerRebuild the classnames utility: strings, arrays, objects, and nested combinations.
Implement createStore (dispatch, subscribe, getState) and a Proxy-based copy-on-write produce in JavaScript — structural sharing, reference equality, and why deep copy is not Immer.
Two-part exercise: (1) implement
createStore(reducer)—getState,dispatch,subscribe; (2) implementproduce(state, recipe)— let reducers write mutation-style code while actually producing immutable updates.
This is the machine-coding question that fuses three catalog threads: closures for the store (the module pattern), pub/sub for subscriptions (event emitter), and Proxy traps for the drafts (negative indexing's big sibling). It's also where a fake answer is easiest to spot — so first, the trap.
The tutorial-circuit version of produce is JSON.parse(JSON.stringify(base)), mutate, return. Three strikes:
undefined and functions vanish, Map/Set become {}.state.x !== prevState.x, so every selector re-fires and every memoized component re-renders. Reference stability isn't a nicety; it's the contract React/Redux performance is built on (memoizeLast is the machinery that consumes it).Bonus strike: seeding the store via produce(undefined, ...) makes JSON.parse(undefined) throw — the deep-copy version often crashes before the first action. If you remember one sentence: Immer is copy-on-write, not copy-everything.
function createStore(reducer, preloadedState) {
let state = preloadedState;
let listeners = new Set();
const getState = () => state;
function dispatch(action) {
state = reducer(state, action); // reducer RETURNS the next state
for (const listener of [...listeners]) listener(); // snapshot — same
return action; // re-entrancy discipline as an emitter
}
function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener); // unsubscribe closure
}
dispatch({ type: "@@INIT" }); // reducer's default-param supplies initial state
return { getState, dispatch, subscribe };
}Small surface, three narratable choices: the reducer returns state (the contract that makes time-travel/devtools possible — state transitions are pure function applications); listeners iterate over a snapshot (a listener that unsubscribes mid-notify must not skip others — the same bug class as emit); subscribe returns the unsubscriber (the leak-prevention idiom useEffect cleanup expects).
produce — copy-on-write with ProxiesThe idea: hand the recipe a draft (a Proxy). Reads pass through to the real state. The first write to any object shallow-copies that object — and tells its parent to copy too, so the modification bubbles up a copied path while untouched branches stay shared:
function produce(base, recipe) {
let result = base; // unchanged until something actually writes
function createDraft(source, onFirstWrite) {
let copy = null; // created lazily, on first write
const current = () => copy ?? source; // reads prefer the copy once it exists
const childDrafts = new Map();
const ensureCopy = () => {
if (copy === null) {
copy = Array.isArray(source) ? [...source] : { ...source };
onFirstWrite(copy); // bubble: parent copies itself and points at us
}
return copy;
};
return new Proxy(source, {
get(_, prop) {
const value = current()[prop];
// Wrap nested objects in their own drafts (lazily, once)
if (typeof value === "object" && value !== null) {
if (!childDrafts.has(prop)) {
childDrafts.set(prop, createDraft(value, (childCopy) => {
ensureCopy()[prop] = childCopy;
}));
}
return childDrafts.get(prop);
}
return value; // primitives and methods (push etc.) pass through
},
set(_, prop, value) {
ensureCopy()[prop] = value; // never touches the original
return true;
},
deleteProperty(_, prop) {
delete ensureCopy()[prop];
return true;
},
has: (_, prop) => prop in current(),
ownKeys: () => Reflect.ownKeys(current()),
getOwnPropertyDescriptor(_, prop) {
const desc = Object.getOwnPropertyDescriptor(current(), prop);
if (desc) desc.configurable = true;
return desc;
},
});
}
const draft = createDraft(base, (rootCopy) => { result = rootCopy; });
const returned = recipe(draft);
return returned !== undefined ? returned : result;
}Two properties fall out that the deep-copy fake cannot produce:
const state = {
todos: [{ text: "a", done: false }, { text: "b", done: false }],
settings: { theme: "dark" },
};
const next = produce(state, (draft) => {
draft.todos[0].done = true;
});
next !== state; // true — changed path copied
next.todos[0] !== state.todos[0]; // true
next.todos[1] === state.todos[1]; // true — SHARED: structural sharing ✓
next.settings === state.settings; // true — untouched branch shared ✓
const same = produce(state, () => {}); // recipe touched nothing
same === state; // true — no-op returns the SAME reference ✓That last check — no writes ⇒ same reference — is what makes dispatch of an unmatched action free for subscribers doing prev === next checks. Array methods work through the traps too: draft.todos.push(x) calls Array.prototype.push with the proxy as this, so its internal reads and writes route through get/set — the copy-on-write happens mid-push without push knowing.
const initialState = { todos: [] };
const todosReducer = (state = initialState, action) =>
produce(state, (draft) => {
switch (action.type) {
case "ADD_TODO":
draft.todos.push({ text: action.text, done: false });
break;
case "TOGGLE_TODO":
draft.todos[action.index].done = !draft.todos[action.index].done;
break;
// no default case needed: an untouched draft returns state AS-IS
}
});
const store = createStore(todosReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: "ADD_TODO", text: "ship it" });This is precisely why Redux Toolkit wraps every reducer in produce: mutation-style ergonomics, spread-free deep updates, immutability guaranteed by construction. Being able to say "RTK's createSlice is produce around your case reducers" places the exercise in the real world.
draft.todos into an outer variable and using it after produce returns is touching a live Proxy over dead state; real Immer revokes drafts on exit (Proxy.revocable) to make this throw. Knowing revocation exists is the hardened answer.Object.freeze sentence.Map/Set/class instances in state — the toy handles plain objects/arrays; real Immer has explicit Map/Set support and an opt-in for classes. Also the honest scope note for this exercise.Object.keys(draft) after adds — works here because ownKeys consults current(); implementations that only trap get/set silently report stale keys — a good "what did you not trap?" probe (minimal-traps discussion).produce (all three strikes, plus the undefined crash).state directly "because produce will fix it" — outside a draft that's just mutation; the discipline is only drafts absorb writes.applyMiddleware)." — wrap dispatch in a compose of store => next => action functions — the curried-middleware signature is currying in the wild.subscribeWithSelector)." — per-listener: run selector, compare with previous via === (structural sharing makes this sound!), notify on change — memoizeLast per subscriber.{ op, path, value } during writes — enables undo/redo and syncing over the wire; connects to the virtual-DOM patch-list idea.{ ...s, a: { ...s.a, b: { ...s.a.b, c } } } at depth 3+ is where produce pays. Cost: Proxy overhead per access — measurable in huge hot loops, irrelevant in reducers. A calibrated answer, not an advocacy answer.=== in connect/useSelector), time-travel debugging, and predictable replay — the three-legged answer that shows you know why the constraint exists rather than treating it as ritual.Rebuild the classnames utility: strings, arrays, objects, and nested combinations.
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.