ng2-redux
This is a fork of NgRedux that adds a provideStore
method
that will hopefully soon get merged into that project.
Angular 2 bindings for Redux.
For Angular 1 see ng-redux
ng2-redux lets you easily connect your Angular 2 components with Redux.
Table of Contents
Installation
npm install --save ng2-redux
Quick Start
Initialization
Import the NgRedux
class and add it to your application as an Angular 2
provider.
import { bootstrap } from '@angular/platform-browser-dynamic';
import { App } from './containers/App';
bootstrap(App, [ NgRedux ]);
Once you've done this, you'll be able to inject 'NgRedux' into your
Angular 2 components. In your top-level app component, you
can configure your Redux store with reducers, initial state,
and optionally middlewares and enhancers as you would in Redux directly.
import { NgRedux } from 'ng2-redux';
import reduxLogger from 'redux-logger';
import { rootReducer } from './reducers';
interface IAppState {
};
@Component({
})
class App {
constructor(private ngRedux: NgRedux) {
this.ngRedux.configureStore(rootReducer, {}, [ reduxLogger() ]);
}
}
Or if you prefer to create the Redux store yourself you can do that and use the
provideStore()
function instead.
Create your store:
import {
applyMiddleware,
Store,
combineReducers,
compose,
createStore
} from 'redux';
import thunk from 'redux-thunk';
import reduxLogger from 'redux-logger';
import { myReducer } from './reducers/my-reducer';
const rootReducer = combineReducers({
myReducer,
});
export const store = createStore(
rootReducer,
compose(
applyMiddleware(
thunk,
reduxLogger
)
)
) as Store
Create your App and call provideStore
with your newly created store:
import { NgRedux } from 'ng2-redux';
import { store } from './store.ts';
interface IAppState {
};
@Component({
})
class App {
constructor(private ngRedux: NgRedux) {
this.ngRedux.provideStore(store);
}
}
Now your Angular 2 app has been reduxified!
Usage
ng2-redux
has two main usage patterns: the select
pattern and the connect
pattern.
The Select Pattern
The select pattern allows you to get slices of your state as RxJS observables.
These plug in very efficiently to Angular 2's change detection mechanism and this is the
preferred approach to accessing store data in Angular 2.
The @select decorator
The @select
decorator can be added to the property of any class or angular
component/injectable. It will turn the property into an observable which observes
the Redux Store value which is selected by the decorator's parameter.
The decorator expects to receive a string
, an array of string
s, a function
or no
parameter at all.
- If a
string
is passed the @select
decorator will attempt to observe a store
property whose name matches the string
. - If an array of strings is passed, the decorator will attempt to match that path
through the store (similar to
immutableJS
's getIn
). - If a
function
is passed the @select
decorator will attempt to use that function
as a selector on the RxJs observable. - If nothing is passed then the
@select
decorator will attempt to use the name of the class property to find a matching value in the Redux store. Note that a utility is in place here where any $ characters will be ignored from the class property's name.
import { Component } from '@angular2/core';
import { AsyncPipe } from '@angular2/common';
import { Observable } from 'rxjs/Observable';
import { select } from 'ng2-redux';
@Component({
pipes: [AsyncPipe],
selector: 'counter-value-printed-many-times',
template: `
<p>{counter$ | async}</p>
<p>{counter | async}</p>
<p>{counterSelectedWithString | async}</p>
<p>{counterSelectedWithFunction | async}</p>
<p>{counterSelectedWithFunctionAndMultipliedByTwo | async}</p>
`
})
export class CounterValue {
@select() counter$;
@select() counter;
@select('counter') counterSelectedWithString;
@select(['pathDemo', 'foo', 'bar']) pathSelection;
@select(state => state.counter) counterSelectedWithFunction;
@select(state => state.counter * 2)
counterSelectedWithFuntionAndMultipliedByTwo: Observable<any>;
}
Select Without Decorators
If you like RxJS, but aren't comfortable with decorators, you can also make
store selections using the ngRedux.select()
function.
import { Component } from '@angular2/core';
import { Observable } from 'rxjs';
import { AsyncPipe } from '@angular2/common';
import { Counter } from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';
import { NgRedux } from 'ng2-redux';
interface IAppState {
counter: number;
};
@Component({
selector: 'root',
directives: [Counter],
pipes: [AsyncPipe],
template: `
<counter [counter]="counter$| async"
[increment]="increment"
[decrement]="decrement">
</counter>
`
})
export class Counter {
private count$: Observable<number>;
constructor(private ngRedux: NgRedux<IAppState>) {}
ngOnInit() {
let {increment, decrement } = CounterActions;
this.counter$ = this.ngRedux.select('counter');
}
incrementIfOdd = () => this.ngRedux.dispatch(
<any>CounterActions.incrementIfOdd());
incrementAsync = () => this.ngRedux.dispatch(
<any>CounterActions.incrementAsync());
}
ngRedux.select
can take a property name or a function which transforms a property.
Since it's an observable, you can also transform data using observable operators like
.map
, .filter
, etc.
The Connect Pattern
Alternately you can use the 'ngRedux.connect' API, which will map your state and action creators
to the component class directly.
This pattern is provided for backwards compatibility. It's worth noting that
Angular 2's view layer is more optimized for Observables and the select
pattern above.
import { Component } from '@angular/core';
import { Counter } from '../components/Counter';
import { NgRedux } from 'ng2-redux';
import { bindActionCreators } from 'redux';
export interface IAppState {
counter: number;
};
const CounterActions = require('../actions/CounterActions');
@Component({
selector: 'root',
directives: [Counter],
template: `
<counter [counter]="counter"
[increment]="actions.increment"
[decrement]="actions.decrement">
</counter>
`
})
export class Counter {
private counter: number;
constructor(private ngRedux: NgRedux<IAppState>) {
ngRedux.connect(this.mapStateToTarget, this.mapDispatchToThis)(this);
}
ngOnDestroy() {
this.disconnect();
}
mapStateToTarget(state) {
return { counter: state.counter };
}
mapDispatchToThis(dispatch) {
return { actions: bindActionCreators(CounterActions, dispatch) };
}
}
A Note about Internet Explorer
This library relies on the presence of Object.assign
and Array.forEach
.
Internet Explorer requires polyfills for these; however these same functions
are also needed for Angular 2 itself to work. Just make sure you include
core-js or es6-shim
if you need to support IE.
Cookbooks
Using Angular 2 Services in your Action Creators
In order to use services in action creators, we need to integrate
them into Angular 2's dependency injector.
We may as well adopt a more class-based approach to satisfy
Angular 2's OOP idiom, and to allow us to
- make our actions
@Injectable()
, and - inject other services for our action creators to use.
Take a look at this example, which injects NgRedux to access
dispatch
and getState
(a replacement for redux-thunk
),
and a simple RandomNumberService
to show a side effect.
import { Injectable } from '@angular/core';
import { NgRedux } from 'ng2-redux';
import * as Redux from 'redux';
import { RootState } from '../store';
import { RandomNumberService } from '../services/random-number';
@Injectable()
export class CounterActions {
constructor (
private ngRedux: NgRedux<RootState>,
private randomNumberService: RandomNumberService) {}
static INCREMENT_COUNTER: string = 'INCREMENT_COUNTER';
static DECREMENT_COUNTER: string = 'DECREMENT_COUNTER';
static RANDOMIZE_COUNTER: string = 'RANDOMIZE_COUNTER';
increment(): void {
this.ngRedux.dispatch({ type: CounterActions.INCREMENT_COUNTER });
}
decrement(): void {
this.ngRedux.dispatch({ type: CounterActions.DECREMENT_COUNTER });
}
incrementAsync(delay: number = 1000): void {
setTimeout(this.increment.bind(this), delay);
}
incrementIfOdd(): void {
const { counter } = this.ngRedux.getState();
if (counter % 2 !== 0) {
this.increment();
}
}
randomize(): void {
this.ngRedux.dispatch({
type: CounterActions.RANDOMIZE_COUNTER,
payload: this.randomNumberService.pick()
});
}
}
To use these action creators, we can just go ahead and inject
them into our component:
import { Component } from '@angular/core';
import { NgRedux, select } from 'ng2-redux';
import { CounterActions } from '../actions/counter-actions';
import { RandomNumberService } from '../services/random-number';
@Component({
selector: 'counter',
providers: [ CounterActions, RandomNumberService ],
template: `
<p>
Clicked: {{ counter$ | async }} times
<button (click)="actions.increment()">+</button>
<button (click)="actions.decrement()">-</button>
<button (click)="actions.incrementIfOdd()">Increment if odd</button>
<button (click)="actions.incrementAsync(2222)">Increment async</button>
<button (click)="actions.randomize()">Set to random number</button>
</p>
`
})
export class Counter {
@select('counter') counter$: any;
constructor(private actions: CounterActions) {}
}
Using Angular 2 Services in your Middleware
Again, we just want to use Angular DI the way it was meant to be used.
Here's a contrived example that fetches a name from a remote API using Angular's
Http
service:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
@Injectable()
export class LogRemoteName {
constructor(private http: Http) {}
middleware = store => next => action => {
console.log('getting user name');
this.http.get('http://jsonplaceholder.typicode.com/users/1')
.toPromise()
.then(response => {
console.log('got name:', response.json().name);
return next(action);
})
.catch(err => console.log('get name failed:', err));
}
}
As with the action example above, we've attached our middleware function to
an @Injectable
class that can itself receive services from Angular's dependency
injector.
Note the arrow function called middleware
: this is what we can pass to the middlewares
parameter when we initialize ngRedux in our top-level component. We use an arrow function
to make sure that what we pass to ngRedux has a properly-bound function context.
import { LogRemoteName } from './middleware/log-remote-name';
import reduxLogger from 'redux-logger';
@Component({
providers: [ LogRemoteName ],
})
class App {
constructor(
private ngRedux: NgRedux
logRemoteName: LogRemoteName) {
const middleware = [ reduxLogger(), logRemoteName.middleware ];
this.ngRedux.configureStore(
rootReducer,
initialState,
middleware);
}
}
Using DevTools
Ng2Redux is fully compatible with the Chrome extension version of the Redux dev tools:
https://github.com/zalmoxisus/redux-devtools-extension
Here's how to enable them in your app (you probably only want to do
this in development mode):
let enhancers = [];
if (__DEVMODE__ && window.devToolsExtension) {
enhancers = [ ...enhancers, window.devToolsExtension() ];
}
@Component({
})
class App {
constructor(private ngRedux: NgRedux) {
this.ngRedux.configureStore(rootReducer, initialState, [], enhancers);
}
}
API
configureStore()
Adds your ngRedux store to NgRedux. This should be called once, typically in your
top-level app component's constructor.
Arguments:
rootReducer
(Reducer): Your top-level Redux reducer.initialState
(*Object): The desired initial state of your store.middleware
(Middleware[]): An optional array of Redux middleware functions.enhancers
(StoreEnhancer[StoreEnhancer]): An optional array of Redux store enhancer functions.
select(key | function,[comparer]) => Observable
Exposes a slice of state as an observable. Accepts either a property name or a selector function.
If using the async pipe, you do not need to subscribe to it explicitly, but can use the angular
Async pipe to bind its values into your template.
Arguments:
key
(string): A key within the state that you want to subscribe to.selector
(Function): A function that accepts the application state, and returns the slice you want subscribe to for changes.
e.g:
this.counter$ = this.ngRedux.select(state=>state.counter);
this.counterSubscription = this.ngRedux
.select(state=>state.counter)
.subscribe(count=>this.counter = count);
this.counter$ = this.ngRedux.select('counter');
provideStore()
Initializes your ngRedux store. This should be called once, typically in your
top-level app component's constructor. If configureStore
has been used this cannot be used.
Arguments:
store
(Store): Your app's store.
select(key | function,[comparer]) => Observable
Exposes a slice of state as an observable. Accepts either a property name or a selector function.
If using the async pipe, you do not need to subscribe to it explicitly, but can use the angular
Async pipe to bind its values into your template.
Arguments:
key
(string): A key within the state that you want to subscribe to.selector
(Function): A function that accepts the application state, and returns the slice you want subscribe to for changes.
e.g:
this.counter$ = this.ngRedux.select(state=>state.counter);
this.counterSubscription = this.ngRedux
.select(state=>state.counter)
.subscribe(count=>this.counter = count);
this.counter$ = this.ngRedux.select('counter');
@select(key | path | function)
Property decorator.
Attaches an observable to the property which will reflect the latest value in the Redux store.
Arguments:
key
(string): A key within the state that you want to subscribe to.path
(string[]): A path of nested keys within the state you want to subscribe to.selector
(Function): A function that accepts the application state, and returns the slice you want to subscribe to for changes.
e.g. see the @select decorator
connect(mapStateToTarget, mapDispatchToTarget)(target)
Connects an Angular component to Redux, and maps action creators and store
properties onto the component instance.
Arguments:
mapStateToTarget
(Function): connect will subscribe to Redux store updates. Any time it updates, mapStateToTarget will be called. Its result must be a plain object, and it will be merged into target
. If you have a component which simply triggers actions without needing any state you can pass null to mapStateToTarget
.- [
mapDispatchToTarget
] (Object or Function): Optional. If an object is passed, each function inside it will be assumed to be a Redux action creator. An object with the same function names, but bound to a Redux store, will be merged onto target
. If a function is passed, it will be given dispatch
. It’s up to you to return an object that somehow uses dispatch
to bind action creators in your own way. (Tip: you may use the bindActionCreators()
helper from Redux.).
You then need to invoke the function a second time, with target
as parameter:
target
(Object or Function): If passed an object, the results of mapStateToTarget
and mapDispatchToTarget
will be merged onto it. If passed a function, the function will receive the results of mapStateToTarget
and mapDispatchToTarget
as parameters.
e.g:
connect(this.mapStateToThis, this.mapDispatchToThis)(this);
connect(this.mapState, this.mapDispatch)((selectedState, actions) => {});
Remarks:
- The
mapStateToTarget
function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. It is often called a selector. Use reselect to efficiently compose selectors and compute derived data.
Store API
All of redux's store methods (i.e. dispatch
, subscribe
and getState
) are exposed by $ngRedux and can be accessed directly. For example:
ngRedux.subscribe(() => {
let state = $ngRedux.getState();
})
This means that you are free to use Redux basic API in advanced cases where connect
's API would not fill your needs.