Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 11 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 => ({
Expand All @@ -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() {
Expand All @@ -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).
41 changes: 17 additions & 24 deletions src/components/connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
}
}
}
10 changes: 8 additions & 2 deletions src/components/ngRedux.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -36,6 +37,11 @@ export default function ngReduxProvider() {
}
}

return Connector(applyMiddleware(...resolvedMiddleware)(_storeEnhancer)(_reducer));
store = applyMiddleware(...resolvedMiddleware)(_storeEnhancer)(_reducer);

return {
...store,
connector: Connector(store)
};
}
}
48 changes: 20 additions & 28 deletions test/components/connector.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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}) => {
Expand All @@ -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);
});

});