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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Splitting history into past, present, and future arrays makes undo and redo just push/pop between three buckets.
- 2Any new edit must invalidate the redo future, since branching forward from an old state is meaningless.
- 3Capping the past array bounds memory so long editing sessions never grow the history without limit.
Related explainers
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
javascript
import { useEffect, useRef, useState } from 'react'; export function useDelayedFlag(active, delay = 300) { const [visible, setVisible] = useState(false);
Delaying a loading spinner with a React hook
custom-hooks
debouncing
cleanup
Intermediate
8 steps
javascript
const SWIPE_THRESHOLD = 80; const MAX_TRANSLATE = 120; export function attachSwipeToDismiss(element, onDismiss) {
Building a swipe-to-dismiss gesture in JS
touch-events
gesture-detection
dom-manipulation
Intermediate
10 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-an-undo-redo-history-stack-explained-javascript-7e3e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.