Skip to content

Commit

Permalink
Merge pull request #25 from darthrellimnad/ignore-branches
Browse files Browse the repository at this point in the history
ignore mutation detection for specified branches
  • Loading branch information
leoasis committed Mar 6, 2017
2 parents bc9aa98 + 1946249 commit 1702b52
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 20 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,26 @@ const store = createStore(
```

Then if you're doing things correctly, you should see nothing different. But if you don't, that is, if you're mutating your data somewhere in your app either in a dispatch or between dispatches, an error will be thrown with a (hopefully) descriptive message.

## API

#### `immutableStateInvariantMiddleware({ isImmutable, ignore })`

Middleware creation function and default export. Supports an optional `object` argument to customize middleware behavior with supported options.

**Parameters**

- **isImmutable** `function` - override default "isImmutable" implementation (see: `src/isImmutable.js`). function must accept a single argument and return `true` if the value should be considered immutable, `false` otherwise.

```js
// example: use a custom `isImmutable` implementation
const mw = immutableStateInvariantMiddleware({ isImmutable: customIsImmutable })
```
- **ignore** `string[]` - specify branch(es) of state to ignore when detecting for mutations. elements of array should be dot-separated "path" strings that match named nodes from the root state.

```js
// example: ignore mutation detection along the 'foo' & 'bar.thingsToIgnore' branches
const mw = immutableStateInvariantMiddleware({ ignore: ['foo', 'bar.thingsToIgnore'] })
```

Returns: `function` - the middleware function
8 changes: 6 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ const INSIDE_DISPATCH_MESSAGE = [
'(http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)'
].join(' ');

export default function immutableStateInvariantMiddleware(isImmutable = isImmutableDefault) {
const track = trackForMutations.bind(null, isImmutable);
export default function immutableStateInvariantMiddleware(options = {}) {
const {
isImmutable = isImmutableDefault,
ignore
} = options
const track = trackForMutations.bind(null, isImmutable, ignore);

return ({getState}) => {
let state = getState();
Expand Down
30 changes: 23 additions & 7 deletions src/trackForMutations.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
export default function trackForMutations(isImmutable, obj) {
const trackedProperties = trackProperties(isImmutable, obj);
export default function trackForMutations(isImmutable, ignore, obj) {
const trackedProperties = trackProperties(isImmutable, ignore, obj);
return {
detectMutations() {
return detectMutations(isImmutable, trackedProperties, obj);
return detectMutations(isImmutable, ignore, trackedProperties, obj);
}
};
}

function trackProperties(isImmutable, obj) {
function trackProperties(isImmutable, ignore = [], obj, path = []) {
const tracked = { value: obj };

if (!isImmutable(obj)) {
tracked.children = {};

for (const key in obj) {
tracked.children[key] = trackProperties(isImmutable, obj[key]);
const childPath = path.concat(key);
if (ignore.length && ignore.indexOf(childPath.join('.')) !== -1) {
continue;
}

tracked.children[key] = trackProperties(
isImmutable,
ignore,
obj[key],
childPath
);
}
}
return tracked;
}

function detectMutations(isImmutable, trackedProperty, obj, sameParentRef = false, path = []) {
function detectMutations(isImmutable, ignore = [], trackedProperty, obj, sameParentRef = false, path = []) {
const prevObj = trackedProperty ? trackedProperty.value : undefined;

const sameRef = prevObj === obj;
Expand All @@ -45,12 +55,18 @@ function detectMutations(isImmutable, trackedProperty, obj, sameParentRef = fals
const keys = Object.keys(keysToDetect);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const childPath = path.concat(key);
if (ignore.length && ignore.indexOf(childPath.join('.')) !== -1) {
continue;
}

const result = detectMutations(
isImmutable,
ignore,
trackedProperty.children[key],
obj[key],
sameRef,
path.concat(key)
childPath
);

if (result.wasMutated) {
Expand Down
43 changes: 35 additions & 8 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ describe('immutableStateInvariantMiddleware', () => {
let state;
const getState = () => state;

function middleware(next) {
return immutableStateInvariantMiddleware()({getState})(next);
function middleware(options) {
return immutableStateInvariantMiddleware(options)({getState});
}

beforeEach(() => {
Expand All @@ -15,7 +15,7 @@ describe('immutableStateInvariantMiddleware', () => {

it('sends the action through the middleware chain', () => {
const next = action => ({...action, returned: true});
const dispatch = middleware(next);
const dispatch = middleware()(next);

expect(dispatch({type: 'SOME_ACTION'})).toEqual({type: 'SOME_ACTION', returned: true});
});
Expand All @@ -26,7 +26,7 @@ describe('immutableStateInvariantMiddleware', () => {
return action;
};

const dispatch = middleware(next);
const dispatch = middleware()(next);

expect(() => {
dispatch({type: 'SOME_ACTION'});
Expand All @@ -36,7 +36,7 @@ describe('immutableStateInvariantMiddleware', () => {
it('throws if mutating between dispatches', () => {
const next = action => action;

const dispatch = middleware(next);
const dispatch = middleware()(next);

dispatch({type: 'SOME_ACTION'});
state.foo.bar.push(5);
Expand All @@ -51,7 +51,7 @@ describe('immutableStateInvariantMiddleware', () => {
return action;
};

const dispatch = middleware(next);
const dispatch = middleware()(next);

expect(() => {
dispatch({type: 'SOME_ACTION'});
Expand All @@ -61,7 +61,7 @@ describe('immutableStateInvariantMiddleware', () => {
it('does not throw if not mutating between dispatches', () => {
const next = action => action;

const dispatch = middleware(next);
const dispatch = middleware()(next);

dispatch({type: 'SOME_ACTION'});
state = {...state, foo: {...state.foo, baz: 'changed!'}};
Expand All @@ -73,7 +73,7 @@ describe('immutableStateInvariantMiddleware', () => {
it('works correctly with circular references', () => {
const next = action => action;

const dispatch = middleware(next);
const dispatch = middleware()(next);

let x = {};
let y = {};
Expand All @@ -84,4 +84,31 @@ describe('immutableStateInvariantMiddleware', () => {
dispatch({type: 'SOME_ACTION', x});
}).toNotThrow();
});

it('respects "isImmutable" option', function () {
const isImmutable = (value) => true
const next = action => {
state.foo.bar.push(5);
return action;
};

const dispatch = middleware({ isImmutable })(next);

expect(() => {
dispatch({type: 'SOME_ACTION'});
}).toNotThrow();
})

it('respects "ignore" option', () => {
const next = action => {
state.foo.bar.push(5);
return action;
};

const dispatch = middleware({ ignore: ['foo.bar'] })(next);

expect(() => {
dispatch({type: 'SOME_ACTION'});
}).toNotThrow();
})
});
87 changes: 84 additions & 3 deletions test/trackForMutations.spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import expect from 'expect';
import isImmutable from '../src/isImmutable';
import isImmutableDefault from '../src/isImmutable';
import trackForMutations from '../src/trackForMutations';

describe('trackForMutations', () => {
function testCasesForMutation(spec) {
it('returns true and the mutated path', () => {
const state = spec.getState();
const tracker = trackForMutations(isImmutable, state);
const options = spec.middlewareOptions || {};
const { isImmutable = isImmutableDefault, ignore } = options;
const tracker = trackForMutations(isImmutable, ignore, state);
const newState = spec.fn(state);

expect(
Expand All @@ -18,7 +20,9 @@ describe('trackForMutations', () => {
function testCasesForNonMutation(spec) {
it('returns false', () => {
const state = spec.getState();
const tracker = trackForMutations(isImmutable, state);
const options = spec.middlewareOptions || {};
const { isImmutable = isImmutableDefault, ignore } = options;
const tracker = trackForMutations(isImmutable, ignore, state);
const newState = spec.fn(state);

expect(
Expand Down Expand Up @@ -163,6 +167,36 @@ describe('trackForMutations', () => {
return s;
},
path: ['foo']
},
'cannot ignore root state': {
getState: () => ({ foo: {} }),
fn: (s) => {
s.foo = {};
return s;
},
middlewareOptions: {
ignore: ['']
},
path: ['foo']
},
'catching state mutation in non-ignored branch': {
getState: () => ({
foo: {
bar: [1, 2]
},
boo: {
yah: [1, 2]
}
}),
fn: (s) => {
s.foo.bar.push(3);
s.boo.yah.push(3);
return s;
},
middlewareOptions: {
ignore: ['foo']
},
path: ['boo', 'yah', '2']
}
};

Expand Down Expand Up @@ -230,6 +264,53 @@ describe('trackForMutations', () => {
'having a NaN in the state': {
getState: () => ({ a:NaN, b: Number.NaN }),
fn: (s) => s
},
'ignoring branches from mutation detection': {
getState: () => ({
foo: {
bar: 'bar'
},
}),
fn: (s) => {
s.foo.bar = 'baz'
return s;
},
middlewareOptions: {
ignore: ['foo']
},
},
'ignoring nested branches from mutation detection': {
getState: () => ({
foo: {
bar: [1, 2],
boo: {
yah: [1, 2]
}
},
}),
fn: (s) => {
s.foo.bar.push(3);
s.foo.boo.yah.push(3);
return s;
},
middlewareOptions: {
ignore: [
'foo.bar',
'foo.boo.yah',
]
}
},
'ignoring nested array indices from mutation detection': {
getState: () => ({
stuff: [{a: 1}, {a: 2}]
}),
fn: (s) => {
s.stuff[1].a = 3
return s;
},
middlewareOptions: {
ignore: ['stuff.1']
}
}
};

Expand Down

0 comments on commit 1702b52

Please sign in to comment.