From 3f4017caafa69a683bca626dbf1a81c2c1e74255 Mon Sep 17 00:00:00 2001 From: Elger Lambert Date: Fri, 21 Aug 2015 18:34:18 +0200 Subject: [PATCH] Refactor ngRedux's api * Expose store methods on ngRedux * Remove getStore * Remove disableCaching from connector --- README.md | 32 +++++++-------------- src/components/connector.js | 41 +++++++++++--------------- src/components/ngRedux.js | 10 +++++-- test/components/connector.spec.js | 48 +++++++++++++------------------ 4 files changed, 56 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index f0429a0..ae9cb53 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ For Angular 2 see [ng2-redux](https://github.com/wbuchwalter/ng2-redux). -#####Warning: The API is unstable and subject to breaking changes. +**Warning: The API is unstable and subject to breaking changes.** [![build status](https://img.shields.io/travis/wbuchwalter/ng-redux/master.svg?style=flat-square)](https://travis-ci.org/wbuchwalter/ng-redux) [![npm version](https://img.shields.io/npm/v/ng-redux.svg?style=flat-square)](https://www.npmjs.com/package/ng-redux) @@ -19,7 +19,7 @@ ngRedux lets you easily connect your angular components with Redux. the API is straightforward: ```JS -$ngRedux.connect(selector, callback, disableCaching = false); +$ngRedux.connect(selector, callback); ``` Where `selector` is a function that takes Redux's entire store state as argument and returns an object that contains the slices of store state that your component is interested in. @@ -34,9 +34,8 @@ If you haven't, check out [reselect](https://github.com/faassen/reselect), an aw This returned object will be passed as argument to the callback provided whenever the state changes. ngRedux checks for shallow equality of the state's selected slice whenever the Store is updated, and will call the callback only if there is a change. -##### Important: It is assumed that you never mutate your states, if you do mutate them, ng-redux will not execute the callback properly. +**Important: It is assumed that you never mutate your states, if you do mutate them, ng-redux will not execute the callback properly.** See [Redux's doc](http://gaearon.github.io/redux/docs/basics/Reducers.html) to understand why you should not mutate your states. -If you have a good reason to mutate your states, you can still [disable caching](#Disable-caching) altogether. ## Getting Started @@ -87,14 +86,14 @@ class TodoLoaderController { } ``` -##### Note: The callback provided to ```connect``` will be called once directly after creation to allow initialization of your component states +**Note: The callback provided to `connect` will be called once directly after creation to allow initialization of your component states** You can also grab multiple slices of the state by passing an array of selectors: ```JS -constructor(reduxConnector) { +constructor($ngRedux) { this.todos = []; this.users = []; $ngRedux.connect(state => ({ @@ -115,9 +114,9 @@ You can close a connection like this: ```JS -constructor(reduxConnector) { +constructor($ngRedux) { this.todos = []; - this.unsubscribe = reduxConnector.connect(state => ({todos: state.todos}), ({todos}) => this.todos = todos); + this.unsubscribe = $ngRedux.connect(state => ({todos: state.todos}), ({todos}) => this.todos = todos); } destroy() { @@ -127,22 +126,13 @@ destroy() { ``` -#### Accessing Redux' Store -You don't need to create another service to get hold of Redux's store (although you can). -You can access the store via ```$ngRedux.getStore()```: +#### Accessing Redux's store methods +All of redux's store methods (i.e. `dispatch`, `subscribe` and `getState`) are exposed by $ngRedux and can be accessed directly. For example: ```JS -redux.bindActionCreators(actionCreator, $ngRedux.getStore().dispatch); +redux.bindActionCreators(actionCreator, $ngRedux.dispatch); ``` - -#### Disabling caching -Each time Redux's Store update, ng-redux will check if the slices specified via 'selectors' have changed, and if so will execute the provided callback. -You can disable this behaviour, and force the callback to be executed even if the slices didn't change by setting ```disableCaching``` to true: - -```JS -reduxConnector.connect(state => ({todos: state.todos}), ({todos}) => this.todos = todos, true); -``` - +**Note:** If you choose to use `subscribe` directly, be sure to [unsubscribe](#unsubscribing) when your current scope is $destroyed. ### Example: An example can be found here (in TypeScript): [tsRedux](https://github.com/wbuchwalter/tsRedux/blob/master/src/components/regionLister.ts). diff --git a/src/components/connector.js b/src/components/connector.js index 9b3ada4..2df9e09 100644 --- a/src/components/connector.js +++ b/src/components/connector.js @@ -3,30 +3,23 @@ import shallowEqual from '../utils/shallowEqual'; import invariant from 'invariant'; export default function Connector(store) { - return { - connect: (selector, callback, disableCaching = false) => { - invariant( - isFunction(callback), - 'The callback parameter passed to connect must be a Function. Instead received %s.', - typeof callback - ); + return (selector, callback) => { + invariant( + isFunction(callback), + 'The callback parameter passed to connect must be a Function. Instead received %s.', + typeof callback + ); - //Initial update - let params = selector(store.getState()); - callback(params); + //Initial update + let params = selector(store.getState()); + callback(params); - let unsubscribe = store.subscribe(() => { - let nextParams = selector(store.getState()); - if (disableCaching || !shallowEqual(params, nextParams)) { - callback(nextParams); - params = nextParams; - } - }); - - return unsubscribe; - }, - getStore() { - return store; - } + return store.subscribe(() => { + let nextParams = selector(store.getState()); + if (!shallowEqual(params, nextParams)) { + callback(nextParams); + params = nextParams; + } + }); } -} \ No newline at end of file +} diff --git a/src/components/ngRedux.js b/src/components/ngRedux.js index 382d1ba..02cb4a1 100644 --- a/src/components/ngRedux.js +++ b/src/components/ngRedux.js @@ -27,7 +27,8 @@ export default function ngReduxProvider() { }; this.$get = ($injector) => { - let resolvedMiddleware = []; + let store, resolvedMiddleware = []; + for(let middleware of _middlewares) { if(typeof middleware === 'string') { resolvedMiddleware.push($injector.get(middleware)); @@ -36,6 +37,11 @@ export default function ngReduxProvider() { } } - return Connector(applyMiddleware(...resolvedMiddleware)(_storeEnhancer)(_reducer)); + store = applyMiddleware(...resolvedMiddleware)(_storeEnhancer)(_reducer); + + return { + ...store, + connector: Connector(store) + }; } } diff --git a/test/components/connector.spec.js b/test/components/connector.spec.js index 64d28ef..72d8c96 100644 --- a/test/components/connector.spec.js +++ b/test/components/connector.spec.js @@ -4,31 +4,34 @@ import Connector from '../../src/components/connector'; describe('Connector', () => { let store; - let connector; + let connect; + beforeEach(() => { - store = createStore((state, action) => { - return {foo: 'bar', baz: action.payload, anotherState: 12}; - }); - connector = Connector(store); + store = createStore((state, action) => ({ + foo: 'bar', + baz: action.payload, + anotherState: 12 + })); + connect = Connector(store); }); it('Should throw when not passed a function as callback', () => { - expect(connector.connect.bind(connector, () => {}, undefined)).toThrow(); - expect(connector.connect.bind(connector, () => {}, {})).toThrow(); - expect(connector.connect.bind(connector, () => {}, 15)).toThrow(); + expect(connect.bind(connect, () => {}, undefined)).toThrow(); + expect(connect.bind(connect, () => {}, {})).toThrow(); + expect(connect.bind(connect, () => {}, 15)).toThrow(); }); it('Callback should be called once directly after creation to allow initialization', () => { let counter = 0; let callback = () => counter++; - connector.connect(state => state, callback); + connect(state => state, callback); expect(counter).toBe(1); }); it('Should call the callback passed to connect when the store updates', () => { let counter = 0; let callback = () => counter++; - connector.connect(state => state, callback); + connect(state => state, callback); store.dispatch({type: 'ACTION', payload: 0}); store.dispatch({type: 'ACTION', payload: 1}); expect(counter).toBe(3); @@ -37,33 +40,23 @@ describe('Connector', () => { it('Should prevent unnecessary updates when state does not change (shallowly)', () => { let counter = 0; let callback = () => counter++; - connector.connect(state => ({baz: state.baz}), callback); + connect(state => ({baz: state.baz}), callback); store.dispatch({type: 'ACTION', payload: 0}); store.dispatch({type: 'ACTION', payload: 0}); store.dispatch({type: 'ACTION', payload: 1}); expect(counter).toBe(3); }); - it('Should disable caching when disableCaching is set to true', () => { - let counter = 0; - let callback = () => counter++; - connector.connect(state => ({baz: state.baz}), callback, true); - store.dispatch({type: 'ACTION', payload: 0}); - store.dispatch({type: 'ACTION', payload: 0}); - store.dispatch({type: 'ACTION', payload: 1}); - expect(counter).toBe(4); - }); - it('Should pass the selected state as argument to the callback', () => { - let receivedState; - connector.connect(state => ({ + connect(state => ({ myFoo: state.foo - }), newState => receivedState = newState); - expect(receivedState).toEqual({myFoo: 'bar'}); + }), newState => { + expect(newState).toEqual({myFoo: 'bar'}); + }); }); it('Should allow multiple store slices to be selected', () => { - connector.connect(state => ({ + connect(state => ({ foo: state.foo, anotherState: state.anotherState }), ({foo, anotherState}) => { @@ -75,11 +68,10 @@ describe('Connector', () => { it('Should return an unsubscribing function', () => { let counter = 0; let callback = () => counter++; - let unsubscribe = connector.connect(state => state, callback); + let unsubscribe = connect(state => state, callback); store.dispatch({type: 'ACTION', payload: 0}); unsubscribe(); store.dispatch({type: 'ACTION', payload: 2}); expect(counter).toBe(2); }); - });