redux-deep-diff
v0.5.0
Published
Higher order reducer to deep diff redux states
Downloads
253
Readme
Redux Deep Diff
deep-diff for tracking structural differences between objects in redux state containers
Also with undo/redo functionality!
Installation
$ yarn add redux-deep-diff
API
redux-deep-diff
is a reducer enhancer (higher-order reducer), it provides the
diff function, which takes an existing reducer and a configuration object and
enhances your existing reducer with deep-diff functionality.
Note: the deep-diff
library calculates structural changes between two
javascript objects. As such, your state must be a diffable object.
To use redux-deep-diff
, first import it, then add the diff enhancer to your
reducer(s) like so:
import { combineReducers } from 'redux';
// redux-deep-diff higher-order reducer
import diff from 'redux-deep-diff';
// `counter` state must be a diffable object
// e.i. state.counter = { count: 1 }
export default combineReducers({
counter: diff(counter, /* [config] */)
})
To configure how diff
enchances your reducers, see the
possible configuration options below.
Diff API
Wrapping your reducer with diff
adds a diff leaf to your state:
{
diff: {
prev: [...previousDiffs],
next: [...futureDiffs]
}
}
Each diff in the history is an array of changes. See deep-diff's differences documentation for more info on change records.
Dispatching undo
& redo
actions
Since redux-deep-diff
tracks changes to your state, you can undo & redo
the diffs in your history.
import { undo, redo, jump } from 'redux-deep-diff';
store.dispatch(undo()); // revert the previous diff
store.dispatch(redo()); // apply the next diff
store.dispatch(jump(-3)); // revert the previous three diffs
store.dispatch(jump(2)); // apply the next two diffs
Dispatching clear
action
import { clear } from 'redux-deep-diff';
store.dispatch(clear()); // clear the history
Deducing state history
To access previous values of the state, redux-deep-diff
has a concept of
deducers
which are similar to selectors in that you create a deducer
and use
it to calculate ("deduce") part of your state's history.
import { createDeducer } from 'redux-deep-diff';
const countSelector = (counter) => counter.count;
const getCountHistory = createDeducer(countSelector, /* [config] */);
// `state.counter` must countain a diff history leaf
getCountHistory(state.counter); //=> [0, 1, 2, 3, 2, ...]
To configure how a deducer
determines which diffs to deduce, see the
possible configuration options below.
Similarly to selectors, deducers
are also memoized and not only memoize their
return value, but the return value of the selector for each set of changes in
the diff history. When the diff history changes, deducers
will only call the
selector with states it hasn't yet deduced.
store.dispatch(increment());
let a = getCountHistory(store.getState());
//=> [0, 1] `countSelector` was called twice
store.dispatch(increment());
store.dispatch(increment());
store.dispatch(decrement());
let b = getCountHistory(store.getState());
//=> [0, 1, 2, 3, 2] `countSelector` was called three more times
// true even for values of objects/arrays
expect(a[0]).to.equal(b[0]);
expect(a[1]).to.equal(b[1]);
let c = getCountHistory(store.getState());
//=> [0, 1, 2, 3, 2] `countSelector` was not called
// when no changes are made, the same results are returned
expect(b).to.equal(c);
// ...but a new array is returned when values do change
expect(a).to.not.equal(b);
Configuration options
Default values for the supported options are listed below
Reducer options
{
key: 'diff', // key to store the state diffs
limit: 0, // diff history limit
undoType: '@@redux-deep-diff/UNDO', // custom undo action type
redoType: '@@redux-deep-diff/REDO', // custom redo action type
jumpType: '@@redux-deep-diff/JUMP', // custom jump action type
clearType: '@@redux-deep-diff/CLEAR', // custom clear action type
skipAction: (action) => false, // return true to skip diffing the state for this action
initialState: { prev: [], next: [] }, // initial diff history state
ignoreInit: true, // includes the first state when `false`
prefilter: (path, key) => false, // (see below)
flatten: (path, key) => false // (see below)
}
prefilter(path, key)
See deep-diff
's prefilter argument.
flatten(path, key)
Similar to prefilter
, flatten
should return a truthy value for any path-key
combination that should be flattened. That is: deep-diff
will not do any
further analysis on the object-property path, but the resulting value of the
change will be the value located at the flattened path.
let flatten = (path, key) => key === 'nested';
/* ...reducer setup... */
expect(state.nested).to.deep.equal({ some: 'property' })
expect(change.path).to.deep.equal(['nested']);
expect(change.lhs).to.deep.equal({ some: 'prev-property' });
expect(change.rhs).to.deep.equal({ some: 'property' });
Deducer options
{
key: 'diff', // key to retrieve the state diffs
next: false, // deduce from `diff.next` when `true` (future history)
unique: false, // skip equal results that immediately follow each other
index: false, // the index of a single state in the history to deduce
range: false, // a range of history states to deduce - `[lower, upper]`
limit: false // limit the deducer to a specified length
}
Important: index
, range
, and limit
may not be used in conjuction. If
they are used together, precedence will be taken in that order and a warning
will be logged.
License
MIT, see LICENSE.md
for more info.