The Problem
When I wanted to add history to drawing-pad I found simple examples about implementing it in Implementing Undo History | Redux.
Btw, I recommend that you read their docs before seeing this one.
While the way that they recommended is great, but when your app state is something crazy (huge), It will cause lag.
For instance, in this app, I wanted to save some paths which will be saved in some way to allow me to apply history.
A path consists of loads of points, so When I saved them the way mentioned in the Redux docs the app got laggy.
So I decided to invent my own way.
// the way mentioned in the docs
interface State {
past: Array<T>;
present: T;
future: Array<T>;
}
This way assumes that T
isn't an array.
In my case T
is of this type
interface Path {
// these are the configs of that this paths was drawn with.
config: {
size: number;
blur: number;
color: number;
};
// these are the points that is path consists of.
points: [number, number][];
}
type T = Path[];
Of course saving a complex data structure like this one in the way mentioned in the docs is a disaster.
The Idea of the solution
So I decided to use a different appraoch which is saving Path
in present
and saving Path[]
in past
and future
.
And the state of the app is the result of merging past
and present
.
interface State {
past: Path[];
present: Path;
future: Path[];
}
This changes of course results some strange cases and changes the common behavior of history.
But, It also gives us some cool feature like that the history will be saved after refreshing.
Implementing the solution
As I said the implementation of this startegy will be different so I will tell you here how did Implemented it.
you should disable the
undo
button if[...past, present]
is empty, and theredo
button iffuture
is empty.
Pushes to the history stack
When present
is null
:
-
Update
present
. -
Reset
future
.
return {
...state,
paths: {
...state.paths,
// 1.
present: action.payload as Path,
// 2.
future: [],
},
};
When present
isn't null
:
-
Push the last
present
topast
. -
Update the current
present
. -
Reset
future
.
return {
...state,
paths: {
// 1.
past: [...state.paths.past, state.paths.present],
// 2.
present: action.payload as Path,
// 3.
future: [],
},
};
Undo
When past
is empty:
-
Insert
present
tofuture
. -
Set
present
tonull
.
return {
...state,
paths: {
// 2.
present: null,
// 1.
future: [state.paths.present!, ...state.paths.future],
},
};
When past
isn't empty:
-
Remove the last item from
past
. -
Set
present
to the removed item frompast
. -
Insert
present
tofuture
.
return {
...state,
paths: {
// 1.
past: state.paths.past.slice(0, -1),
// 2.
present: state.paths.past[state.paths.past.length - 1],
// 3.
future: [state.paths.present!, ...state.paths.future],
},
}
Redo
When present
is null
:
-
Remove the first item from
future
. -
Set
present
to the removed item fromfuture
.
return {
...state,
paths: {
past: state.paths.past,
// 2.
present: state.paths.future[0],
// 1.
future: state.paths.future.slice(1),
},
};
When present
isn't null
:
-
Remove the first item from
future
. -
Set
present
to the removed item fromfuture
. -
Push
present
topast
.
return {
...state,
paths: {
// 3.
past: [...state.paths.past, state.paths.present!],
// 2.
present: state.paths.future[0],
// 1.
future: state.paths.future.slice(1),
},
}
Conclusion
I know that the implementation isn't straight forward and is (kind of) complicated, but this is the only way that I managed to find.
If you want to see it in a real-world example you can check the code of drawing-pad reducer function 👋.