State Management — Build the Library Yourself
A Store in 200 Lines
Goal
You will solve by hand the problem that Redux, Zustand, and Jotai all solve in common. One file, under 200 lines.
What to build
In /root/work/state/store.mjs, export createStore.
export function createStore(initial) {
return { getState, dispatch, subscribe, select }
}
| Function | Contract |
|---|---|
getState() |
Returns the current state immediately |
dispatch(fn) |
The return value of fn(현재) is the new state (the placeholder is the current state). It does not modify the original |
subscribe(l) |
Returns an unsubscribe function |
select(sel) |
Returns sel(현재) (the placeholder is the current state), but does not recompute when the state is unchanged |
How grading works
The grader imports your store.mjs and checks the contract one item at a time.
It uses no browser and no framework.
node --version # v22
node store.mjs # 문법 확인용 (아무것도 출력 안 해도 됩니다)
Steps
getState·dispatch- No in-place mutation
subscribe→ unsubscribe function- Don't notify on the same reference
select— compute derived values- Memoization
- Microtask batching
- Safe to unsubscribe during iteration
- Wrap-up →
09-notes.md
Notes
- npm install doesn't work. There is no network, and you don't need it.
- The shape of the state is up to you. The grader passes in a plain
object such as
{ n: 0, items: [] }and checks only the contract.
The skeleton of the store
In /root/work/state/store.mjs, export createStore(initial). The returned object must have getState() and dispatch(fn), and dispatch takes the return value of fn(현재상태) as the new state (the placeholder is the current state).
Run mkdir -p /root/work/state. It is ESM, so write export function createStore(initial) { ... }. Because the extension is .mjs, you don't need a package.json. Don't mutate the state in place inside dispatch — the next step checks for that.
Block in-place mutation
Make sure dispatch never modifies the state object it receives. The grader compares the original object before and after dispatch.
Guarding yourself with Object.freeze is also a good approach — if you modify it by mistake, you get an error instead of a silent no-op. Always update state with a new object ({...s, n: s.n+1}).
Subscribe and unsubscribe
Add subscribe(listener) and return an unsubscribe function. After unsubscribing, the listener must not be called again.
The return value must be a function. Unsubscribing must be safe no matter how many times it is called (a common pitfall is an implementation where calling it twice removes a different listener).
If nothing changed, don't notify
When the same reference comes back, as with dispatch(s => s), don't call the listeners.
It is one line: if (next === prev) return. Without it, the state stays the same but a render runs, and if that render dispatches again, it becomes an infinite loop.
Compute derived state
Add select(selector). store.select(s => s.items.length) must always return a value based on the current state. You must not store derived values in the state.
It is tempting to put a field like count in the state, but then a moment comes when the two disagree. The grader changes only items and then checks whether the derived value follows.
Don't recompute for the same input
Add memoization to select. If the state hasn't changed and you ask several times with the same selector, the selector function must run only once.
Remember the last state reference and the last result for each selector (using a Map or WeakMap). This is where immutability earns its keep — because the state is immutable, a single reference comparison tells you "nothing changed."
Change it three times in one tick, render once
Bundle consecutive dispatches into a single microtask. After dispatch(a); dispatch(b); dispatch(c), the listener must be called exactly once, and the state at that time must be the final value with all three changes applied.
A queueMicrotask call and one scheduled flag are enough. This is what React 18's automatic batching is. Note: getState() must return the latest value immediately, regardless of batching — you must not defer the dispatch itself.
Stay safe when unsubscribing during iteration
Make sure that unsubscribing inside a listener does not cause other listeners to be skipped.
[...listeners].forEach(...) — iterate over a copy. It is one line, but without it you get the bug "sometimes one listener isn't called," which has a low reproduction rate and is hard to trace.
Why libraries look the way they do
In 09-notes.md, write at least three lines: what immutability made O(1), what goes out of sync if you store derived state, and what is wasted without batching.
The text must contain 불변, 파생, and 배치 (the Korean words for "immutable", "derived", and "batch"). These three are problems that Redux, Zustand, and Vue solve in the same way even though they look different from one another.