A form starts with two useState calls. Then an address arrives, followed by location coordinates, preferences, and a few counters. Soon every input has its own setter or a slightly different object-spread expression. AlgoFly's video explores a smaller interface: one state object, with reads and updates expressed as paths.
The linked public gist calls this helper reactState and demonstrates updates such as state.set("address.city", "Delhi"). The appealing part is the caller's experience. This article explains that experiment, points out the behavior in the published code that needs care, and offers an illustrative immutable version. It is a component-state convenience, not an automatic replacement for every state-management library.

Why path-based reads and updates feel convenient
The video description describes replacing several state hooks with one custom hook, reading nested properties, and observing updates through effects. A path acts as a compact address inside the state tree. A profile editor can update name, address.city, or address.location.lat through the same interface.
“Without a predefined schema” means fields can be introduced as the UI needs them. It does not mean the data has no structure or that invalid values become valid. You still need decisions about required fields, number parsing, API contracts, and the difference between a missing field and an empty one.
The original example uses useReducer with an object-merging reducer. That places the state inside React; it is not a module-level external store shared across every component.
Do not lose false, zero, or an empty string
A nested getter must preserve valid falsy values. The gist's dotted-path reader uses truthiness checks and || null, so a nested 0, false, or "" can be turned into null. For a counter or checkbox, that changes the meaning of the data.
A better read operation follows each key only when the current value is not null or undefined. It should return the stored value, even when that value is false. Missing paths can return undefined, leaving the caller to choose a display fallback with ??.
function getPath(value, path) {
return path.split(".").reduce((current, key) => current?.[key], value);
}
getPath({ preferences: { enabled: false } }, "preferences.enabled");
// false
Copy every ancestor along the modified path
The original writer walks the existing object and assigns into it before sending the result to the reducer. A shallow spread at the reducer's top level does not undo those nested mutations. Existing references to an address object still point at the object that was changed.
For a path update, copy the root and every object on the route to the leaf. Untouched branches can retain their references. This keeps React's snapshot model intact and gives memoized children and effect dependencies a meaningful identity change.
The following helper deliberately supports plain-object paths only. Arrays, keys containing dots, removal, and complex object types need an explicit policy. It also rejects prototype-related path names instead of accepting arbitrary external paths as object assignments.
const blocked = new Set(["__proto__", "prototype", "constructor"]);
function setPath(source, path, update) {
const keys = path.split(".");
if (keys.some(key => !key || blocked.has(key))) {
throw new Error("Invalid state path");
}
function visit(node, depth) {
const key = keys[depth];
const copy = { ...(node ?? {}) };
if (depth === keys.length - 1) {
copy[key] = typeof update === "function" ? update(node?.[key]) : update;
} else {
copy[key] = visit(node?.[key], depth + 1);
}
return copy;
}
return visit(source, 0);
}
Apply functional updates to the latest state
The gist includes a counter call that passes a function to set, but its writer assigns the function as the stored value rather than evaluating it against the previous leaf. A convenient API should decide whether functions are values or updater callbacks; this example chooses updater callbacks.
Using React's functional setter also avoids basing a sequence of updates on a stale render's object. Here is a minimal hook built around the helper above. The use prefix makes its role clear to React tooling.
import { useCallback, useState } from "react";
export function usePathState(initial = {}) {
const [value, setValue] = useState(initial);
const set = useCallback((path, update) => {
setValue(previous => setPath(previous, path, update));
}, []);
return { value, set, get: path => getPath(value, path) };
}
// Inside a component:
// const state = usePathState({ count: 0 });
// state.set("count", previous => previous + 1);
Observe the value your effect actually needs
Reading a leaf in an effect dependency list can work, but it is clearer to assign that leaf to a local variable first. If an effect needs latitude, depend on latitude rather than an entire address object. If it needs the full location, immutable updates ensure that replacing location produces a new reference.
Reset semantics need the same precision. The published reducer resets to an empty object. A form may instead need its initial values. Neither is universally correct; name the operation so a caller knows whether “reset” means clear everything or restore a starting snapshot.
Use the abstraction where it removes real repetition
A path helper is useful for small dynamic forms, prototype interfaces, and metadata editors. It becomes less attractive when arbitrary string paths hide business rules or allow invalid combinations of fields. Named reducer actions such as addressSelected can express a coordinated update more clearly.
For shared state outside React, use a subscription mechanism designed for external stores, such as useSyncExternalStore, rather than assuming this local hook provides cross-component synchronization. For server-rendered applications, do not create one mutable singleton that accidentally shares a user's state with another request.
The original experiment remains useful because it makes a common source of repetition visible. Keep the simple calling style, then make the semantics explicit: preserve false values, update immutable snapshots, handle updater callbacks correctly, and choose a narrower tool when the data has stronger rules.
