omnidan / redux-undo

:recycle: higher order reducer to add undo/redo functionality to redux state containers

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

undoables are not pure functions?

bjornicus opened this issue · comments

I was seeing some strange behavior in my app where the first time I dispatched an action which applied to my undoable the past array had a single empty array (was [[]] ) instead of containing what as previously the present state. Trying to simplify things down I came up with the following test case, which seems to indicate that undoable reducers aren't in fact pure functions

import undoable, { includeAction } from 'redux-undo';

function rawReducer(state = [], action) {
  return [...state];
}

let undoableReducer = undoable(rawReducer, {
  limit: 10,
  filter: includeAction('ANY')
});

let initialState = {
  past: [],
  present: [1, 2],
  future: [],
  group: null,
  _latestUnfiltered: [],
  index: 0,
  limit: 1
};

test('this test passes', () => {
  expect(
    undoableReducer(initialState, {
      type: 'ANY',
      playerId: 1,
      currentTime: 0
    }).past
  ).toEqual([initialState.present]);
});

test('this identical test does not (because it runs after the first one)', () => {
  expect(
    undoableReducer(initialState, {
      type: 'ANY',
      playerId: 1,
      currentTime: 0
    }).past
  ).toEqual([initialState.present]);
});

The first test passes, but the second one fails with:

    Expected value to equal:
      [[1, 2]]
    Received:
      [[]]

@bjornicus did you solve this? I've been working on a solution using pure functions here: https://github.com/gamb/fast-undo, any testing / feedback would be greatly appreciated :)

I don't actually remember if I worked around this or not, but thanks for the link; I'll definitely check it out next time I'm doing something redux undoable.

commented

I think your problem first originated because present !== _latestUnfiltered. You can fix it by setting them to the same object.

const present = [1,2]
let initialState = {
  present,
  _latestUnfiltered: present,
  ...
};

Long story short, undoable functions are not pure until after initialization:

undoableReducer(initialState || undefined, { type: '@@INIT' })

The reason the second test fails is that the empty list _latestUnfiltered (not present) is pushed into past when an action is dispatched.

The first test only passes because the undoableReducer has to initialize a starting state that is retained by closure. When that is done, a new history is created and _latestUnfiltered is changed to present. You can see this in action if you set the config option debug: true.

For more consistent testing, I would initialize the reducer with a dummy state before running any tests. Looking at some of the tests might serve as an example.