**Approach (brief)** Use a history stack + current index in reducer state. Store either full snapshots or reversible patches; combine both for performance. Support UNDO/REDO by moving index and applying stored state/patches. For selective undo, partition state into branches and maintain per-branch history or store tagged patches and apply inverse only for matching branch.**Recommended state shape**- history: Array of { snapshot?, patches?:[], meta?: { branch?: string } }- index: number- present: current state (redundant for fast read)Example:javascript
const initial = { history: [{ snapshot: initialState }], index: 0, present: initialState };
function reducer(state, action) {
switch(action.type){
case 'APPLY_CHANGE': {
const { patch, snapshot, meta } = action.payload;
const nextPresent = snapshot ?? applyPatch(state.present, patch);
const nextHistory = state.history.slice(0, state.index+1).concat({ snapshot, patches: patch ? [patch] : undefined, meta });
return { history: nextHistory, index: state.index+1, present: nextPresent };
}
case 'UNDO': {
if (state.index === 0) return state;
const prev = state.history[state.index-1].snapshot ?? reconstruct(state.history, state.index-1);
return { ...state, index: state.index-1, present: prev };
}
case 'REDO': {
if (state.index === state.history.length-1) return state;
const next = state.history[state.index+1].snapshot ?? reconstruct(state.history, state.index+1);
return { ...state, index: state.index+1, present: next };
}
case 'SELECTIVE_UNDO': {
// find last history entry with meta.branch === action.payload.branch and revert only that branch
const targetIndex = findLastIndex(state.history, state.index, e => e.meta?.branch === action.payload.branch);
if (targetIndex < 0) return state;
const branchPrev = extractBranch(reconstruct(state.history, targetIndex), action.payload.branch);
const newPresent = { ...state.present, [action.payload.branch]: branchPrev };
// Append a new history entry representing this selective change
const nextHistory = state.history.slice(0, state.index+1).concat({ snapshot: newPresent, meta: { selective: true, branch: action.payload.branch } });
return { history: nextHistory, index: state.index+1, present: newPresent };
}
}
}
**Key concepts / trade-offs**- Snapshots: simple, fast restore, high memory.- Patches (deltas): smaller memory, need apply/inverse logic, complexity to reconstruct.- Hybrid: take periodic snapshots and store patches between them for efficient rebuilds.**Complexity & best practices**- Time: UNDO/REDO O(1) if snapshots; O(k) to reconstruct when applying patches (k = patches since last snapshot). - Space: snapshots = O(n * stateSize), patches = O(n * patchSize). - Use structural sharing (immer/immutable.js) to reduce memory. Limit history length and compress/TTL old entries. Tag changes with branch meta to enable selective undo and auditing.