npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ngrx-undoable

v1.2.0

Published

[Redux](https://github.com/reactjs/redux)/[Ngrx](https://github.com/ngrx) implementation of [Undo/Redo](http://redux.js.org/docs/recipes/ImplementingUndoHistory.html) based on Actions instead of States

Downloads

151

Readme

ngrx-undoable

Redux/Ngrx implementation of Undo/Redo based on Actions instead of States

License: MIT TypeScript

Installation

npm

npm install ngrx-undoable --save

yarn

yarn add ngrx-undoable --save

How it works

If an action is dispatched it will call your reducer, then store the resulting state in present and add the action to the past.

If an Undo action is dispatched, it calulcates the new state by replaying all the past actions and stores the action that was undone in the future.

If a Redo action is dispatched, it reduces the present with the future action to calculate the new present. The action that was redone is then added to the past.

In order for this to work, your reducer has to be pure!

Usage

import { undoable } from 'ngrx-undoable'
const undoableReducer = undoable(reducer, initAction).reducer

reducer is the reducer which you want to add undo and redo functionality to and initAction is the action which initializes your reducer.

undoableReducer is the Undoable Reducer, it works just like your reducer except that the state now looks like this:

{
  past    : [ initAction ]
  present : state
  future  : [ ]
}

Every action you dispatch will be added to the past. Lets look at the popular counter example.

const undoableCounter = undoable(counter).reducer
const undoableSelectors = undoable(counter).selectors

const initialState = undoableCounter(undefined, { type: 'INIT_UNDOABLE_COUNTER' })

{
  past    : [ { type: 'INIT' } ]
  present : 0
  future  : [ ]
}

const state1 = undoableCounter(initialState, { type: 'INCREMENT' })

{
  past    : [ { type: 'INIT' }, { type: 'INCREMENT' } ]
  present : 1
  future  : [ ]
}

const state2 = undoableCounter(state1, { type: 'DECREMENT' })

{
  past    : [ { type: 'INIT' }, { type: 'INCREMENT' }, { type: 'DECREMENT' } ]
  present : 0
  future  : [ ]
}

Selectors

const undoableSelectors = undoable(counter).selectors

These are your selectors to query the undoable state, use them!! Do not just select the state via state.past or state.present!

The selectors are the contract between this library and your code they won't change and guarantee that I won't break your app when adding features to this library.

State Selectors

undoableSelectors.getPastStates(state)

// An Array of State objects that represent the past in the order: [oldest, latest]
[ 0, 1 ]
undoableSelectors.getPresentState(state)

// The current State
2
undoableSelectors.getFutureStates(state)

// An Array of State objects that represent the future in the order: [latest, oldest]
[ 3, 4 ]

Action Selectors

undoableSelectors.getPastActions(state)

// An Array of Action objects that represent the past in the order: [oldest, latest]
[ { type: 'INIT' }, { type: 'INCREMENT' } ]
undoableSelectors.getPresentAction(state)

// The latest Action
{ type: 'INCREMENT' }
undoableSelectors.getFutureActions(state)

// An Array of Action objects that represent the future in the order: [latest, oldest]
[ { type: 'INCREMENT' }, { type: 'INCREMENT' } ]

Custom Selectors

Of course you can create your own selectors, but make sure you use the existing ones as an input for your new ones f.e. using reselect:

createSelector(getPastStates, pastStates => pastStates.filter(x => x > 1))

Undo

There are two recommended ways to create an undo action:

  1. Use the action creator
import { undo } from 'ngrx-undoable'
const undoAction = undo()
  1. Use the UndoableTypes
import { UndoableTypes } from 'ngrx-undoable'
const undoAction = { type: UndoableTypes.UNDO }
const initialState =
{
  past    : [ { type: 'INIT' }, { type: 'INCREMENT' }, { type: 'DECREMENT' } ]
  present : 0
  future  : [ ]
}

undoableCounter(initialState, undoAction)

{
  past    : [ { type: 'INIT' }, { type: 'INCREMENT' } ]
  present : 1
  future  : [ { type: 'DECREMENT' } ]
}

The payload of the undo action corresponds to the number of steps to undo, it defaults to 1. If the payload is greater than past.length, all actions will be undone.

undoableCounter(initialState, undo(2))

{
  past    : [ { type: 'INIT' },  ]
  present : 0
  future  : [ { type: 'DECREMENT' }, { type: 'INCREMENT' } ]
}

Redo

Redo works pretty much analogous to undo:

import { redo } from 'ngrx-undoable'

const initialState =
{
  past    : [ { type: 'INIT' }, { type: 'INCREMENT' } ]
  present : 1
  future  : [ { type: 'DECREMENT' } ]
}

undoableCounter(initialState, redo())

{
  past    : [ { type: 'INIT' }, { type: 'INCREMENT' }, { type: 'DECREMENT' } ]
  present : 1
  future  : [ ]
}

Group

The group action is a sepcial undoable action. It will group the actions given in the payload, and store them as an array inside the past. Undo will then undo them as one single step.

import { group } from 'ngrx-undoable'
const incrementTwice = group({ type: 'INCREMENT' }, { type: 'INCREMENT' })
import { UndoableTypes } from 'ngrx-undoable'
const incrementTwice = { type: UndoableTypes.GROUP, payload: [ { type: 'INCREMENT' }, { type: 'INCREMENT' } ] }
const state1 = undoableCounter(initialState, incrementTwice)

{
  past    : [ { type: 'INIT' }, [ { type: 'INCREMENT' }, { type: 'INCREMENT' } ] ]
  present : 2
  future  : [ ]
}

const state2 = undoableCounter(state1, undo(1))

{
  past    : [ { type: 'INIT' } ]
  present : 0
  future  : [ [ { type: 'INCREMENT' }, { type: 'INCREMENT' } ] ]
}

Optional Parameters

initAction

You may have wondered where { type: 'INIT' } inside the past comes from. Its the default initAction with which your reducer is called when it is initialized.

But you may provide a custom init action via the second parameter to undoable.

const undoableCounter = undoable(counter, { type: 'MY_CUSTOM_INIT' })

{
  past    : [ { type: 'MY_CUSTOM_INIT' } ]
  present : 0
  future  : [ ]
}

Comparator

The third argument of undoable is a comparator function which compares two states in order to detect state changes.

  • If it evaluates to true, the action history is not updated and the state is returned.
  • If it evaluates to false, the action history is updated and the new state is returned.
  • The default comparator uses strict equality (s1, s2) => s1 === s2.
  • To add every action to the history one would provide the comparator (s1, s2) => false.
undoable(counter, initAction, (s1, s2) => false)

Motivation

TL:DR It really just boils down to if your state is fat and your actions are thin or your state is thin and your actions are fat.

  • Use redux-undo if your state is thin and your actions are fat.
  • Use undox if your state is fat and your actions are thin and you want maximum performance for that.
  • Use this library if you want something in between with a nicer API than undox. (only present state is stored)

The most popular and used library to add undo/redo functionality to redux is without a doubt redux-undo.

It stores the whole state instead of actions. While this is great if we got a lean state and fat actions, it does not scale well if our state tree grows.

This library instead only stores actions, which results in some nice advantages, but also some disadvantages depending on your use case.

Advantages

  • Takes up less space inside localStorage for thin actions and fat states
  • Better performance for thin actions and fat states
  • A complete history for free!
  • Type safety (completely written in TypeScript)
  • Smaller in size than redux-undo

Disadvantages

  • Takes up more space inside localStorage for fat actions and thin states
  • Worse performance for fat actions and thin states
  • Less feature rich than redux-undo