preface
A previous post in the community, Undo Redo Implementation for Web Applications, detailed several ideas for undo redo implementation. I learned about the Immer library through the comments section, so I went to understand and practice it, and combined with my current daily development needs, I developed a Dva plug-in to achieve undo redo.
Introduction to plug-in
Plug-in address:
Github.com/frontdog/dv…
1. Instantiate the plug-in
import dva from 'dva';
import createUndoRedo from 'dva-immer-undo-redo';
const app = dva();
/ /... Other plug-ins are used first, making sure this plug-in is last
app.use(
createUndoRedo({
include: ['namespace'].namespace: 'timeline' // The default is timeline})); app.router(...) ; app.start(...) ;Copy the code
Options for the plug-in have three configurable options:
options.namespace
: Optional. Default value:timeline
, the namespace that stores undo redo state. Default state:
// state.timeline
{
canRedo: false.canUndo: false.undoCount: 0.redoCount: 0,}Copy the code
options.include
: Mandatory. Namespace that you want to implement undo redo.options.limit
: Optional. Default value:1024
To set the limit on the number of undo redo stacks.
2. Cancel the Undo
dispatch({ type: 'timeline/undo' })
Copy the code
3. Redo Redo
dispatch({ type: 'timeline/redo' })
Copy the code
4. Reducer has built-in Immer by default
In the Reducer, we already have Immer built in, so you can work directly on state, for example:
// models/counter.js
{
namespace: 'counter'.state: {
count: 0,},reducers: {
add(state) {
state.count += 1; }}}Copy the code
This saves you from having to construct immutable data yourself and return newState, making the Reducer code simpler.
The principle is introduced
The plug-in structure
export default (options) => {
return {
_handleActions,
onReducer,
extraReducers,
};
}
Copy the code
Three hooks of Dva plug-in are used here: _handleActions, onReducer and extraReducers
ExtraReducers initializes the default state
ExtraReducers is an additional reducer when createStore is created, and then the reducer is aggregated by combineReducers, so that state is automatically aggregated during initialization.
export default (options = {}) => {
const { namespace } = options;
const initialState = {
canUndo: false.canRedo: false.undoCount: 0.redoCount: 0};return {
// ...
extraReducers: {
[namespace](state = initialState) {
returnstate; }}}; }Copy the code
_handleActions Built-in Immer and collection of patches
_handleActions gives you the ability to extend all reducer’s, which we use here, in conjunction with immer, to turn the original Reducer state into a draft. Meanwhile, in the third parameter, we can collect patches of a change.
import immer, { applyPatches } from 'immer';
export default (options = {}) => {
const { namespace } = options;
constinitialState = {... };let stack = [];
let inverseStack = [];
return {
// ...
_handleActions(handlers, defaultState) {
return (state = defaultState, action) = > {
const { type } = action;
const result = immer(state, (draft) => {
const handler = handlers[type];
if (typeof handler === 'function') {
const compatiableRet = handler(draft, action);
if(compatiableRet ! = =undefined) {
return compatiableRet;
}
}
}, (patches, inversePatches) => {
if (patches.length) {
const namespace = type.split('/') [0];
if(newOptions.include.includes(namespace) && ! namespace.includes('@ @')) {
inverseStack = [];
if (action.clear === true) {
stack = [];
} else if (action.replace === true) {
const stackItem = stack.pop();
if (stackItem) {
const { patches: itemPatches, inversePatches: itemInversePatches } = stackItem;
patches = [...itemPatches, ...patches];
inversePatches = [...inversePatches, ...itemInversePatches]
}
}
if(action.clear ! = =true) { stack.push({ namespace, patches, inversePatches }); } stack = stack.slice(-newOptions.limit); }}});return result === undefined? {} : result; }; }}; }Copy the code
OnReducer Enables undo redo
OnReducer this hook can achieve high order Reducer function, refer to direct use of Redux, equivalent to:
const originReducer = (state, action) => state;
const reduxReducer = onReducer(originReducer);
Copy the code
With higher-order functions, we can hijack some actions for special processing:
import immer, { applyPatches } from 'immer';
export default (options = {}) => {
const { namespace } = options;
constinitialState = {... };let stack = [];
let inverseStack = [];
return {
// ...
onReducer(reducer) {
return (state, action) = > {
let newState = state;
if (action.type === `${namespace}/undo`) {
const stackItem = stack.pop();
if (stackItem) {
inverseStack.push(stackItem);
newState = immer(state, (draft) => {
const { namespace: nsp, inversePatches } = stackItem; draft[nsp] = applyPatches(draft[nsp], inversePatches); }); }}else if (action.type === `${namespace}/redo`) {
const stackItem = inverseStack.pop();
if (stackItem) {
stack.push(stackItem);
newState = immer(state, (draft) => {
const { namespace: nsp, patches } = stackItem; draft[nsp] = applyPatches(draft[nsp], patches); }); }}else if (action.type === `${namespace}/clear`) {
stack = [];
inverseStack = [];
} else {
newState = reducer(state, action);
}
return immer(newState, (draft: any) => {
const canUndo = stack.length > 0;
const canRedo = inverseStack.length > 0;
if(draft[namespace].canUndo ! == canUndo) { draft[namespace].canUndo = canUndo; }if(draft[namespace].canRedo ! == canRedo) { draft[namespace].canRedo = canRedo; }if(draft[namespace].undoCount ! == stack.length) { draft[namespace].undoCount = stack.length; }if(draft[namespace].redoCount ! == inverseStack.length) { draft[namespace].redoCount = inverseStack.length; }}); }; }}; }Copy the code
In this function, we intercept three actions: Namespace /undo, namespace/redo, and namespace/clear, and then operate on the state according to patches collected previously to implement undo redo.
Here, we can also make some changes to the overall state after the normal reducer, which is used to change canRedo, canUndo and other states.