Build a custom React hook that manages key/value pairs in state. A Map is the natural structure for "look up a value by its key" — a cart keyed by product id, form errors keyed by field name, a selection keyed by row id. But Map is mutable: map.set(k, v) and map.delete(k) change the existing object in place. React decides whether to re-render by checking whether you handed it a new value, not by inspecting the contents — so a mutated Map looks unchanged and the screen goes stale. useMap(initialEntries) owns the Map and hands back the current map plus four helpers that change it the right way: set, remove, get, and clear.
function useMap<K, V>(initialEntries?: Array<[K, V]> | Map<K, V>): {
map: Map<K, V>;
set: (key: K, value: V) => void;
remove: (key: K) => void;
get: (key: K) => V | undefined;
clear: () => void;
};
initialEntries defaults to an empty list when omitted, and accepts either an array of [key, value] pairs or an existing Map. set adds or overwrites an entry; remove deletes the entry for a key; get reads the value for a key (or undefined if absent); clear empties the Map.
function PriceList() {
const { map, set, remove, clear } = useMap([['apple', 1]]);
return (
<div>
{[...map].map(([name, price]) => (
<button key={name} onClick={() => remove(name)}>{name}: {price} x</button>
))}
<button onClick={() => set('banana', 2)}>add banana</button>
<button onClick={clear}>clear</button>
</div>
);
}
// shows apple: 1; "add banana" -> apple: 1, banana: 2; clicking "apple" -> banana: 2
// Each mutating helper returns a brand-new Map; the old one is never changed.
const { map, set, remove, get } = useMap([['a', 1]]);
set('b', 2); // Map { a => 1, b => 2 }
set('a', 99); // Map { a => 99, b => 2 } (overwrites)
get('a'); // 99
get('missing'); // undefined
remove('a'); // Map { b => 2 }
map.set(k, v) changes the existing Map and keeps the same reference. React compares the new value to the old by reference, sees no change, and skips the re-render — so the screen freezes.new Map(prev), then apply the change to the copy. new Map(prev) is a shallow copy, which is exactly what reference-equality needs.get is a plain read. It returns from the current Map and does not trigger a re-render; missing keys return undefined, not an error.remove, not delete. delete is a reserved word in JavaScript, awkward to expose as a method, so the hook returns it as remove.You'll wrap one Map in state and a small bundle of helpers, each of which rebuilds the Map as a brand-new copy instead of editing the old one in place.
Lots of interfaces need to look something up by a key: a shopping cart keyed by product id, validation errors keyed by field name, which rows are selected keyed by row id. A Map is built for exactly that. A custom hook lets you write the moves — add an entry, overwrite one, drop one, read one, wipe them all — once and reuse them everywhere. The catch is that a Map is mutable: map.set(k, v) and map.delete(k) change the Map you already have. React doesn't look inside the Map to decide whether to re-render; it only checks whether you handed it a different Map than before. So the whole job of useMap is to make every change produce a new Map.
React stores your Map in state as a single reference — think of it as a label pointing at a box of entries. When a setter runs, React compares the new label against the old one. If they point at the same box, React assumes nothing changed and skips the re-render, even if you secretly added an entry inside. map.set(k, v) does exactly that: it drops an entry into the existing box and returns the same label. The fix is to always build a new box — new Map(prev) copies every existing entry into a fresh Map — apply the change there, and point the label at it. Different label, so React re-renders.
The instinct is to use the Map methods you already know — set to add, delete to remove — and then push the Map back into state:
const { useState } = require('react');
function useMap(initialEntries = []) {
const [map, setMap] = useState(() => new Map(initialEntries));
const set = (key, value) => {
map.set(key, value); // mutates the existing Map
setMap(map); // hands React the SAME reference
};
const remove = (key) => {
map.delete(key);
setMap(map);
};
const get = (key) => map.get(key);
const clear = () => setMap(new Map());
return { map, set, remove, get, clear };
}
For a single call this often looks like it works in development, which makes the bug so sneaky. But map.set(key, value) mutates the existing Map, and setMap(map) then passes React the very same reference it already holds. React compares old and new, finds them identical, and bails out of the re-render — so map on screen never updates. The view silently freezes. Worse, two set calls in one event both read and mutate the same captured map, so what lands on screen depends on render timing rather than on what you asked for.
const { useState } = require('react');
function useMap(initialEntries = []) {
// Lazy initializer: build the Map once on the first render. Passing a function
// means new Map(initialEntries) doesn't re-run on every later render.
const [map, setMap] = useState(() => new Map(initialEntries));
// Copy the previous Map into a new one, then set on the COPY. new Map(prev)
// is a fresh reference, so React re-renders. The functional updater (prev) => ...
// receives the latest Map React is about to apply, so two sets in one batch
// stack instead of both reading the same stale snapshot. Map.prototype.set
// returns the Map, so we can return it inline.
const set = (key, value) =>
setMap((prev) => new Map(prev).set(key, value));
// delete has no useful return value, so copy, delete on the copy, return it.
const remove = (key) =>
setMap((prev) => {
const next = new Map(prev);
next.delete(key);
return next;
});
// A plain read off the current Map — no copy, no setState, no re-render.
const get = (key) => map.get(key);
// Replacing everything doesn't depend on the old Map, so a fresh empty Map is fine.
const clear = () => setMap(new Map());
return { map, set, remove, get, clear };
}
module.exports = { useMap };
The shift is purely in how each change is built. Every mutating helper now copies first — new Map(prev) returns a fresh Map and leaves the original alone — so React always sees a new reference and re-renders. And by passing a function to setMap rather than a value, each updater reads the latest Map React is about to apply, so several helper calls in one event compose correctly instead of colliding on a stale snapshot. get stays a plain read because reading never needs to trigger a render.
Start with useMap([['a', 1]]). The first render runs the lazy initializer new Map([['a', 1]]), so map is Map { a => 1 }, and the hook returns the four helpers. Now a click handler fires set('b', 2) and then remove('a') in the same event:
setMap((prev) => new Map(prev).set('b', 2)) is queued. React will call this updater with the latest pending Map. Pending starts at Map { a => 1 }, so this copies it and adds b, producing Map { a => 1, b => 2 }.setMap((prev) => { const next = new Map(prev); next.delete('a'); return next; }) is queued behind it. React calls it with Map { a => 1, b => 2 } — the pending value after step 1 — copies it, deletes a, and returns Map { b => 2 }.useState returns Map { b => 2 }, so map is Map { b => 2 } and the screen updates.Each step returned a brand-new Map, so React never bailed out, and the functional updaters chained so remove saw the set's result rather than the render-time Map { a => 1 }.
map.set(k, v); setMap(map) hands React the Map it already holds, so the reference is unchanged and React skips the re-render — the view freezes. Fix: copy first with setMap((prev) => new Map(prev).set(k, v)).setMap(new Map(map).set(k, v)) reads the render-time map, so two sets in one event both start from the same snapshot and one is lost. Fix: use the functional form setMap((prev) => new Map(prev).set(k, v)).remove. next.delete(key) returns a boolean (whether the key existed), not the Map. If you write return next.delete(key) you store true/false in state. Fix: delete on its own line, then return next.new Map(initialEntries) every render. useState(new Map(initialEntries)) rebuilds the Map on every render and throws the result away after the first. Fix: pass a function — useState(() => new Map(initialEntries)) — so it runs once.useCallback (with an empty dependency array, since the functional updaters need no dependencies) keeps the same reference across renders.setAll(entries), has(key), toggle(key, value), or a bulk merge(otherMap) — each follows the same rule of returning a new Map copied from the previous one.useReducer with action types like { type: 'set', key, value } keeps the update logic in one place and makes batched, interdependent changes easier to reason about than four separate setters.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build a custom React hook that manages key/value pairs in state. A Map is the natural structure for "look up a value by its key" — a cart keyed by product id, form errors keyed by field name, a selection keyed by row id. But Map is mutable: map.set(k, v) and map.delete(k) change the existing object in place. React decides whether to re-render by checking whether you handed it a new value, not by inspecting the contents — so a mutated Map looks unchanged and the screen goes stale. useMap(initialEntries) owns the Map and hands back the current map plus four helpers that change it the right way: set, remove, get, and clear.
function useMap<K, V>(initialEntries?: Array<[K, V]> | Map<K, V>): {
map: Map<K, V>;
set: (key: K, value: V) => void;
remove: (key: K) => void;
get: (key: K) => V | undefined;
clear: () => void;
};
initialEntries defaults to an empty list when omitted, and accepts either an array of [key, value] pairs or an existing Map. set adds or overwrites an entry; remove deletes the entry for a key; get reads the value for a key (or undefined if absent); clear empties the Map.
function PriceList() {
const { map, set, remove, clear } = useMap([['apple', 1]]);
return (
<div>
{[...map].map(([name, price]) => (
<button key={name} onClick={() => remove(name)}>{name}: {price} x</button>
))}
<button onClick={() => set('banana', 2)}>add banana</button>
<button onClick={clear}>clear</button>
</div>
);
}
// shows apple: 1; "add banana" -> apple: 1, banana: 2; clicking "apple" -> banana: 2
// Each mutating helper returns a brand-new Map; the old one is never changed.
const { map, set, remove, get } = useMap([['a', 1]]);
set('b', 2); // Map { a => 1, b => 2 }
set('a', 99); // Map { a => 99, b => 2 } (overwrites)
get('a'); // 99
get('missing'); // undefined
remove('a'); // Map { b => 2 }
map.set(k, v) changes the existing Map and keeps the same reference. React compares the new value to the old by reference, sees no change, and skips the re-render — so the screen freezes.new Map(prev), then apply the change to the copy. new Map(prev) is a shallow copy, which is exactly what reference-equality needs.get is a plain read. It returns from the current Map and does not trigger a re-render; missing keys return undefined, not an error.remove, not delete. delete is a reserved word in JavaScript, awkward to expose as a method, so the hook returns it as remove.You'll wrap one Map in state and a small bundle of helpers, each of which rebuilds the Map as a brand-new copy instead of editing the old one in place.
Lots of interfaces need to look something up by a key: a shopping cart keyed by product id, validation errors keyed by field name, which rows are selected keyed by row id. A Map is built for exactly that. A custom hook lets you write the moves — add an entry, overwrite one, drop one, read one, wipe them all — once and reuse them everywhere. The catch is that a Map is mutable: map.set(k, v) and map.delete(k) change the Map you already have. React doesn't look inside the Map to decide whether to re-render; it only checks whether you handed it a different Map than before. So the whole job of useMap is to make every change produce a new Map.
React stores your Map in state as a single reference — think of it as a label pointing at a box of entries. When a setter runs, React compares the new label against the old one. If they point at the same box, React assumes nothing changed and skips the re-render, even if you secretly added an entry inside. map.set(k, v) does exactly that: it drops an entry into the existing box and returns the same label. The fix is to always build a new box — new Map(prev) copies every existing entry into a fresh Map — apply the change there, and point the label at it. Different label, so React re-renders.
The instinct is to use the Map methods you already know — set to add, delete to remove — and then push the Map back into state:
const { useState } = require('react');
function useMap(initialEntries = []) {
const [map, setMap] = useState(() => new Map(initialEntries));
const set = (key, value) => {
map.set(key, value); // mutates the existing Map
setMap(map); // hands React the SAME reference
};
const remove = (key) => {
map.delete(key);
setMap(map);
};
const get = (key) => map.get(key);
const clear = () => setMap(new Map());
return { map, set, remove, get, clear };
}
For a single call this often looks like it works in development, which makes the bug so sneaky. But map.set(key, value) mutates the existing Map, and setMap(map) then passes React the very same reference it already holds. React compares old and new, finds them identical, and bails out of the re-render — so map on screen never updates. The view silently freezes. Worse, two set calls in one event both read and mutate the same captured map, so what lands on screen depends on render timing rather than on what you asked for.
const { useState } = require('react');
function useMap(initialEntries = []) {
// Lazy initializer: build the Map once on the first render. Passing a function
// means new Map(initialEntries) doesn't re-run on every later render.
const [map, setMap] = useState(() => new Map(initialEntries));
// Copy the previous Map into a new one, then set on the COPY. new Map(prev)
// is a fresh reference, so React re-renders. The functional updater (prev) => ...
// receives the latest Map React is about to apply, so two sets in one batch
// stack instead of both reading the same stale snapshot. Map.prototype.set
// returns the Map, so we can return it inline.
const set = (key, value) =>
setMap((prev) => new Map(prev).set(key, value));
// delete has no useful return value, so copy, delete on the copy, return it.
const remove = (key) =>
setMap((prev) => {
const next = new Map(prev);
next.delete(key);
return next;
});
// A plain read off the current Map — no copy, no setState, no re-render.
const get = (key) => map.get(key);
// Replacing everything doesn't depend on the old Map, so a fresh empty Map is fine.
const clear = () => setMap(new Map());
return { map, set, remove, get, clear };
}
module.exports = { useMap };
The shift is purely in how each change is built. Every mutating helper now copies first — new Map(prev) returns a fresh Map and leaves the original alone — so React always sees a new reference and re-renders. And by passing a function to setMap rather than a value, each updater reads the latest Map React is about to apply, so several helper calls in one event compose correctly instead of colliding on a stale snapshot. get stays a plain read because reading never needs to trigger a render.
Start with useMap([['a', 1]]). The first render runs the lazy initializer new Map([['a', 1]]), so map is Map { a => 1 }, and the hook returns the four helpers. Now a click handler fires set('b', 2) and then remove('a') in the same event:
setMap((prev) => new Map(prev).set('b', 2)) is queued. React will call this updater with the latest pending Map. Pending starts at Map { a => 1 }, so this copies it and adds b, producing Map { a => 1, b => 2 }.setMap((prev) => { const next = new Map(prev); next.delete('a'); return next; }) is queued behind it. React calls it with Map { a => 1, b => 2 } — the pending value after step 1 — copies it, deletes a, and returns Map { b => 2 }.useState returns Map { b => 2 }, so map is Map { b => 2 } and the screen updates.Each step returned a brand-new Map, so React never bailed out, and the functional updaters chained so remove saw the set's result rather than the render-time Map { a => 1 }.
map.set(k, v); setMap(map) hands React the Map it already holds, so the reference is unchanged and React skips the re-render — the view freezes. Fix: copy first with setMap((prev) => new Map(prev).set(k, v)).setMap(new Map(map).set(k, v)) reads the render-time map, so two sets in one event both start from the same snapshot and one is lost. Fix: use the functional form setMap((prev) => new Map(prev).set(k, v)).remove. next.delete(key) returns a boolean (whether the key existed), not the Map. If you write return next.delete(key) you store true/false in state. Fix: delete on its own line, then return next.new Map(initialEntries) every render. useState(new Map(initialEntries)) rebuilds the Map on every render and throws the result away after the first. Fix: pass a function — useState(() => new Map(initialEntries)) — so it runs once.useCallback (with an empty dependency array, since the functional updaters need no dependencies) keeps the same reference across renders.setAll(entries), has(key), toggle(key, value), or a bulk merge(otherMap) — each follows the same rule of returning a new Map copied from the previous one.useReducer with action types like { type: 'set', key, value } keeps the update logic in one place and makes batched, interdependent changes easier to reason about than four separate setters.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.