sudheeshcm
sudheeshcm

Reputation: 3428

What is the best way to implement undo state change (undo store/history implementation) in React Redux

I'm looking out for a history redo/ store reset to the previous state in a redux react application.

I've found a blog telling it can be done by storing present, future and past states in a stack and resetting accordingly.

I've also found a similar question in StackOverflow, but it doesn't give me a proper answer or maybe it's difficult for me to understand.

I've built a demo ToDo app and have used redux-logger to log store details with previous state and updated state. You can find the code here.

Do we have a store reset method in redux, so that we can get the previous state and update the store other that having a store with the present, the past and future states?

Upvotes: 8

Views: 13697

Answers (3)

Praveen
Praveen

Reputation: 2419

For anyone looking for a solution to this in 2020. You don't have to store the entire state object as the present, past, and future.

Instead, you can just store details about what has changed. This can be implemented using ImmerJS. It records all changes done on the state object and generates something called a patch.

Example: If age is updated from 32 to 40, then the generated patch will be:

Patch: [ { op: 'replace', path: [ 'age' ], value: 40 } ]

Inverse Patch: [ { op: 'replace', path: [ 'age' ], value: 32 } ]

It also exposes a method to apply these patches/inverse patches to the state - applyPatch. So, to undo we can apply an inverse patch and to redo we can apply a patch.

You can find details of full implementation here: Implementing Undo-Redo Functionality in Redux using Immer

Upvotes: 11

GibboK
GibboK

Reputation: 73978

What is the best way ...

Best way is always difficult to define, it really depends of your use-case and requirements included client and server.

But to get start you you could consider using a library or looking how they approach this problem, some example:

https://github.com/omniscientjs/immstruct

https://www.npmjs.com/package/redux-undo

https://github.com/PowToon/redux-undo-redo

Tutorial with example of todo undo/redo in redux: https://github.com/reactjs/redux/tree/master/examples/todos-with-undo

Or you could implement your own, as redux you could store all your application state. A simple stack could be a simple and efficient way to store your app state at any given time.

let yourHistory = [state1, state2, state3];

Upvotes: 6

Dane Iracleous
Dane Iracleous

Reputation: 1759

I created a state undo/redo snapshot manager class, which would be great for tracking the change history on an HTML element.

  <div id="buttons">
     <button type="button" id="undo_btn">Undo</button>
     <button type="button" id="redo_btn">Redo</button>
  </div>
  <br/><br/>
  <div id="content">
     <label>
        Input1:
        <input type="text" value="" />
     </label>
     <br/><br/>
     <label>
        Input2:
        <input type="text" value="" />
     </label>
     <br/><br/>
     <label>
        Input3:
        <input type="text" value="" />
     </label>
     <br/><br/>
     <label>
        Input4:
        <input type="text" value="" />
     </label>
     <br/><br/>
  </div>

  <script type="text/javascript">
  var StateUndoRedo = function() {
     var init = function(opts) {
        var self = this;
        self.opts = opts;
        if(typeof(self.opts['undo_disabled']) == 'undefined') {
           self.opts['undo_disabled'] = function() {};
        }
        if(typeof(self.opts['undo_enabled']) == 'undefined') {
           self.opts['undo_enabled'] = function() {};
        }
        if(typeof(self.opts['redo_disabled']) == 'undefined') {
           self.opts['redo_disabled'] = function() {};
        }
        if(typeof(self.opts['redo_enabled']) == 'undefined') {
           self.opts['redo_enabled'] = function() {};
        }
        if(typeof(self.opts['restore']) == 'undefined') {
           self.opts['restore'] = function() {};
        }
        self.opts['undo_disabled']();
        self.opts['redo_disabled']();
     }

     var add = function(state) {
        var self = this;
        if(typeof(self.states) == 'undefined') {
           self.states = [];
        }
        if(typeof(self.state_index) == 'undefined') {
           self.state_index = -1;
        }
        self.state_index++;
        self.states[self.state_index] = state;
        self.states.length = self.state_index + 1;
        if(self.state_index > 0) {
           self.opts['undo_enabled']();
        }
        self.opts['redo_disabled']();
     }

     var undo = function() {
        var self = this;
        if(self.state_index > 0) {
           self.state_index--;
           if(self.state_index == 0) {
              self.opts['undo_disabled']();
           } else {
              self.opts['undo_enabled']();
           }
           self.opts['redo_enabled']();

           self.opts['restore'](self.states[self.state_index]);
       }
     }

     var redo = function() {
        var self = this;
        if(self.state_index < self.states.length) {
           self.state_index++;
           if(self.state_index == self.states.length - 1) {
              self.opts['redo_disabled']();
           } else {
              self.opts['redo_enabled']();
           }
           self.opts['undo_enabled']();

           self.opts['restore'](self.states[self.state_index]);
       }
     }

     var restore = function() {
        var self = this;
        self.opts['restore'](self.states[self.state_index]);
     }

     var clear = function() {
        var self = this;
        self.state_index = 0;
        //self.states = [];
     }

     return {
        init: init,
        add: add,
        undo: undo,
        redo: redo,
        restore: restore,
        clear: clear
     };
  };

  //initialize object
  var o = new StateUndoRedo();
  o.init({
     'undo_disabled': function() {
        //make the undo button hidden
        document.getElementById("undo_btn").disabled = true;
     },
     'undo_enabled': function() {
        //make the undo button visible
        document.getElementById("undo_btn").disabled = false;
     },
     'redo_disabled': function() {
        //make the redo button hidden
        document.getElementById("redo_btn").disabled = true;
     },
     'redo_enabled': function() {
        //make the redo button visible
        document.getElementById("redo_btn").disabled = false;
     },
     'restore': function(state) {
        //replace the current content with the restored state content
        document.getElementById("content").innerHTML = state;
     }
  });

  //initialize first state
  o.add(document.getElementById("content").innerHTML);
  o.restore();
  o.clear();

  //bind click events for undo/redo buttons
  document.getElementById("undo_btn").addEventListener("click", function() {
     o.undo();
  });
  document.getElementById("redo_btn").addEventListener("click", function() {
     o.redo();
  });

  //bind change events for content element
  document.getElementById('content').addEventListener("change", function(event) {
     // the following is required since vanilla JS innerHTML 
     // does not capture user-changed values of inputs
     // so we set the attributes explicitly (use jQuery to avoid this)
     var elems = document.querySelectorAll("#content input");
     for(var i = 0; i < elems.length; i++) {
        elems[i].setAttribute("value", elems[i].value);
     }

     //take a snapshot of the current state of the content element
     o.add(document.getElementById("content").innerHTML);
  });
  </script>

See this JSFiddle: https://jsfiddle.net/up73q4t0/56/

Upvotes: -1

Related Questions