javascript 51 lines · 7 steps

Building an undo/redo history stack

A three-bucket past/present/future model gives any app clean undo, redo, and debounced state capture.

Explained by highlit
1class HistoryStack {
2 constructor(initial = '', limit = 100) {
3 this.past = [];
4 this.present = initial;
5 this.future = [];
6 this.limit = limit;
7 this.pending = null;
8 }
9 
10 push(next) {
11 if (next === this.present) return;
12 this.past.push(this.present);
13 if (this.past.length > this.limit) this.past.shift();
14 this.present = next;
15 this.future = [];
16 }
17 
18 pushDebounced(next, delay = 400) {
19 clearTimeout(this.pending);
20 this.pending = setTimeout(() => this.push(next), delay);
21 }
22 
23 undo() {
24 if (!this.canUndo) return this.present;
25 clearTimeout(this.pending);
26 this.future.unshift(this.present);
27 this.present = this.past.pop();
28 return this.present;
29 }
30 
31 redo() {
32 if (!this.canRedo) return this.present;
33 this.past.push(this.present);
34 this.present = this.future.shift();
35 return this.present;
36 }
37 
38 get canUndo() {
39 return this.past.length > 0;
40 }
41 
42 get canRedo() {
43 return this.future.length > 0;
44 }
45 
46 clear() {
47 clearTimeout(this.pending);
48 this.past = [];
49 this.future = [];
50 }
51}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Splitting history into past, present, and future arrays makes undo and redo just push/pop between three buckets.
  2. 2Any new edit must invalidate the redo future, since branching forward from an old state is meaningless.
  3. 3Capping the past array bounds memory so long editing sessions never grow the history without limit.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building an undo/redo history stack — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code