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
undobutton if[...past, present]is empty, and theredobutton iffutureis 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
presenttopast. -
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
presenttofuture. -
Set
presenttonull.
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
presentto the removed item frompast. -
Insert
presenttofuture.
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
presentto 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
presentto the removed item fromfuture. -
Push
presenttopast.
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 👋.