Home

Awesome

Mobx Shallow Undo Build Status Available on NPM

Part of HTTP Toolkit: powerful tools for building, testing & debugging HTTP(S)

Drop-in undo & redo for shallow changes in Mobx

Mobx-shallow-undo is a tiny zero-dependency library for shallow undo/redo on any mobx observable value.

If you have some changing but internally immutable state anywhere in your app - from core state in your app-wide store down to individual observable fields on your React components - this lets you immediately add undo/redo to that state, and freely flip back and forth through its whole history, with zero hassle or configuration required.

For example, HTTP Toolkit uses this to support undo/redo in an autocompleting tagged input field, where undo behaviour needs to manage some of the state in the component, not just the text in the input field itself.

This is designed for simple cases, where a single observable value (or a single property of an observable object) is changing between different immutable values. By default, this does not handle mutations inside the value or track undo states recursively, it just tracks the shallow undo/redo state of an observable. To track nested properties you can use .toJS() as a workaround.

TL;DR: Mobx-shallow-undo lets you do this:

import * as mobx from 'mobx';
import { trackUndo } from 'mobx-shallow-undo';

const myObservable = mobx.observable({ a: 1 });

const myUndoer = trackUndo(
    // Getter, this will be observed:
    () => myObservable.a,
    // Setter, to reset the value on undo/redo:
    (value) => { myObservable.a = value }
);

myObservable.a = 2;
myObservable.a = 3;
myObservable.a = 1000;

myUndoer.undo();
myUndoer.undo();

// myObservable = { a: 2 };

myUndoer.redo();
// myObservable = { a: 3 };

Handle nested data structures like this:

import React from "react";
import { render } from "react-dom";
import * as mobx from "mobx";
import { trackUndo } from "mobx-shallow-undo";

const myObservable = mobx.observable.array([{ id: 1 }, { id: 2 }]);

const myUndoer = trackUndo(
  // .toJS() was added to observe a nested structure
  () => mobx.toJS(myObservable),
  (value) => {
    myObservable.replace(value);
  }
);

myObservable.push({ id: 3 });

// myObservable = [{ id: 1 }, { id: 2 }, { id: 3 }];

myUndoer.undo();

// myObservable = [{ id: 1 }, { id: 2 }];

Using observable.array and its replace method is not required. The example above will continue to work with the modern makeAutoObservable Mobx API.

Getting Started

npm install mobx-shallow-undo
import { trackUndo } from 'mobx-shallow-undo';

const undoer = trackUndo(
    () => /* Read the undoable observable */,
    (value) => { /* Update the undoable observable */ }
);

undoer.undo(); // Undo the last change, if possible
undoer.redo(); // Redo the last undo, if possible

API

trackUndo(getter, setter)

Creates an undoer. getter and setter must be synchronous functions to get and set an observable value.

The getter takes no arguments, whilst the setter takes one argument: the new value.

This returns an undoer.

undoer.undo()

Undoes the latest change. If the undo stack is empty this does nothing.

undoer.redo()

Redoes the latest undo. If we're already on the latest change this does nothing.

Changes to the observed value after an undo clear the redo stack, just like undo in every other application.

undoer.dispose()

Stops observing the observable and throws away all historical data.

Any future calls to undo() or redo() will throw an error.