What’s up everyone? I’m wiring up a little UI state store and I’m trying to keep a normalized entity map, but my optimistic updates keep leaking mutations into other views (and then rollback gets weird).
type Entity = { id: string; name: string; version: number };
type State = {
entities: Record<string, Entity>;
lists: Record<string, string[]>; // listId -> entity ids
};
function optimisticRename(state: State, id: string, name: string): State {
// I know this is sketchy, but it’s fast:
state.entities[id].name = name;
return state;
}
Given that I also need retries/rollback and don’t want to deep-clone the whole state every time, what mutation strategy or data structure pattern do you actually use here to keep updates isolated and predictable?
Mutating state.entities[id] is how you end up with spooky action-at-a-distance, because some component somewhere cached that object reference and now it “updated” without you meaning to.
What’s worked for me is treating optimistic updates like overlays: keep your base entities immutable-ish, and store optimistic edits separately as tiny patches keyed by opId (and usually indexed by entityId too). Your read path resolves “base entity + any active patches for this id”, so the only thing you copy during an optimistic rename is the one entity (or even just the patch object), and rollback is just dropping that opId patch. When the server confirms, you commit by writing a new {...entity, name} into entities[id] (copy-on-write for that entry) and remove the patch; retries don’t touch base state at all.
Bonus: since your lists are just id arrays, they stay totally stable and you avoid the “why did this list rerender” mystery.