Skip to content

Debounce of the digests triggered by store modification #175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 24, 2018
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ es
coverage
*.tgz
examples/**/dist
.idea
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ For Angular 2 see [ng2-redux](https://github.com/wbuchwalter/ng2-redux).
- [API](#api)
- [Dependency Injectable Middleware](#dependency-injectable-middleware)
- [Routers](#routers)
- [Config](#config)
- [Using DevTools](#using-devtools)
- [Additional Resources](#additional-resources)

Expand Down Expand Up @@ -188,6 +189,29 @@ $ngReduxProvider.createStoreWith(reducers, [thunk, 'myInjectableMiddleware']);

Middlewares passed as **string** will then be resolved throught angular's injector.

## Config

### Debouncing the digest
You can debounce the digest triggered by store modification (usefull in huge apps with a lot of store modifications) by passing a config parameter to the `ngReduxProvider`.

```javascript
import angular from 'angular';

angular.module('ngapplication').config(($ngReduxProvider) => {
'ngInject';

// eslint-disable-next-line
$ngReduxProvider.config.debounce = {
wait: 100,
maxWait: 500,
};
});
```

This will debounce the digest for 100ms with a maximum delay time of 500ms. Every store modification within this time will be handled by this delayed digest.

[lodash.debounce](https://lodash.com/docs/4.17.4#debounce) is used for the debouncing.

## Routers
See [redux-ui-router](https://github.com/neilff/redux-ui-router) to make ng-redux and UI-Router work together. <br>
See [ng-redux-router](https://github.com/amitport/ng-redux-router) to make ng-redux and angular-route work together.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"babel-runtime": "^6.26.0",
"invariant": "^2.2.2",
"lodash.curry": "^4.1.1",
"lodash.debounce": "^4.0.8",
"lodash.isfunction": "^3.0.8",
"lodash.isobject": "^3.0.2",
"lodash.isplainobject": "^4.0.6",
Expand Down
16 changes: 12 additions & 4 deletions src/components/digestMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
export default function digestMiddleware($rootScope) {
import debounce from 'lodash.debounce';

export default function digestMiddleware($rootScope, debounceConfig) {
let debouncedFunction = (expr) => {
$rootScope.$evalAsync(expr);
};
if(debounceConfig && debounceConfig.wait && debounceConfig.wait > 0) {
debouncedFunction = debounce(debouncedFunction, debounceConfig.wait, { maxWait: debounceConfig.maxWait });
}
return store => next => action => {
const res = next(action);
$rootScope.$evalAsync(res);
return res;
const res = next(action);
debouncedFunction(res);
return res;
};
}
11 changes: 9 additions & 2 deletions src/components/ngRedux.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ export default function ngReduxProvider() {
_initialState = initialState || {};
};

this.config = {
debounce: {
wait: undefined,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we do a default debounce of 1ms, would it be possible for us to batch updates "per tick" essentially? I'm thinking of how you might queue several actions at the same time and instead of digesting each one immediately, we'd let it all run on the next tick/frame/whatever.

I'm not sure if $evalAsync does this already or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "per tick" should be already covered by the $evalAsync.

I think that 1ms won't make a real performance gain.

Here we set it to {wait: 100, maxWait: 500} for a gain of ~50% of digests.

maxWait: undefined,
},
};

this.$get = ($injector) => {
const resolveMiddleware = middleware => isString(middleware)
? $injector.get(middleware)
Expand Down Expand Up @@ -71,13 +78,13 @@ export default function ngReduxProvider() {
}

// digestMiddleware needs to be the last one.
resolvedMiddleware.push(digestMiddleware($injector.get('$rootScope')));
resolvedMiddleware.push(digestMiddleware($injector.get('$rootScope'), this.config.debounce));

// combine middleware into a store enhancer.
const middlewares = applyMiddleware(...resolvedMiddleware);

// compose enhancers with middleware and create store.
const store = createStore(_reducer, _initialState, compose(...resolvedStoreEnhancer, middlewares));
const store = createStore(_reducer, _initialState, compose(middlewares, ...resolvedStoreEnhancer));

return assign({}, store, { connect: Connector(store) });
};
Expand Down
64 changes: 64 additions & 0 deletions test/components/digestMiddleware.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import expect from 'expect';
import sinon from 'sinon';
import digestMiddleware from '../../src/components/digestMiddleware';


describe('digestMiddleware', () => {

it('Should debounce the $evalAsync function if debounce is enabled', (done) => {
const $evalAsync = sinon.spy();
const $rootScope = {
$evalAsync,
};
const firstAction = 1;
const secondAction = 2;
const debounceConfig = {
wait: 10,
};
const next = sinon.spy((action) => (action));
const middleware = digestMiddleware($rootScope, debounceConfig);
middleware()(next)(firstAction);
setTimeout(() => {
middleware()(next)(secondAction);
}, 1);
setTimeout(() => {
expect($evalAsync.calledOnce).toBe(true);
expect(next.calledTwice).toBe(true);
expect(next.firstCall.calledWithExactly(firstAction)).toBe(true);
expect(next.secondCall.calledWithExactly(secondAction)).toBe(true);
expect($evalAsync.firstCall.calledWithExactly(secondAction)).toBe(true);
done();
}, debounceConfig.wait + 10);

});

it('Should not debounce the $evalAsync function if debounce is disabled', () => {
const disabledDebounceConfigs = [
null,
undefined,
{},
{ wait: 0 },
];
disabledDebounceConfigs.forEach(() => {
const $evalAsync = sinon.spy();
const $rootScope = {
$evalAsync,
};
const firstAction = 1;
const secondAction = 2;
const debounceConfig = {};

const next = sinon.spy((action) => (action));
const middleware = digestMiddleware($rootScope, debounceConfig);
middleware()(next)(firstAction);
middleware()(next)(secondAction);
expect($evalAsync.calledTwice).toBe(true);
expect(next.calledTwice).toBe(true);
expect(next.firstCall.calledWithExactly(firstAction)).toBe(true);
expect(next.secondCall.calledWithExactly(secondAction)).toBe(true);
expect($evalAsync.firstCall.calledWithExactly(firstAction)).toBe(true);
expect($evalAsync.secondCall.calledWithExactly(secondAction)).toBe(true);
});
});

});