-
What is ...
Redux is a popular and common approach to manage a application state. The three principles of redux are:
This package helps you to integrate Redux in your Angular 2+ application. By using ngx-redux you'll get the following benefits:
- support for lazy loaded NgModules
- Ahead-of-Time Compilation (AOT) support
- a Angular Pipe to select the values from the state
- better typescript and refactoring support
- a decorator and module driven approach
- easy to test
First you need to install
- "redux" as peer dependency
- the "@harmowatch/ngx-redux-core" package itself
npm install redux @harmowatch/ngx-redux-core --save
To use ngx-redux in your Angular project you have to import ReduxModule.forRoot()
in the root NgModule of your
application.
The static forRoot
method is a convention
that provides and configures services at the same time. Make sure you call this method in your root NgModule, only!
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReduxModule } from '@harmowatch/ngx-redux-core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
ReduxModule.forRoot(),
],
bootstrap: [ AppComponent ]
})
export class AppModule {
}
1.1 Bootstrap your own Redux Store
By default ngx-redux will bootstrap a Redux Store for you. Is the app running in devMode, the default store is prepared to work together with the Redux DevTools.
If you want to add a Middleware like logging, you've to provide a custom stateFactory.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReduxModule, ReduxModuleRootReducer } from '@harmowatch/ngx-redux-core';
import { applyMiddleware, createStore, Store, StoreEnhancer } from 'redux';
import logger from 'redux-logger';
import { AppComponent } from './app.component';
export function enhancerFactory(): StoreEnhancer<{}> {
return applyMiddleware(logger);
}
export function storeFactory(): Store<{}> {
return createStore(
ReduxModuleRootReducer.reduce,
{},
enhancerFactory()
);
}
@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
ReduxModule.forRoot({
storeFactory,
}),
],
bootstrap: [ AppComponent ]
})
export class AppModule {
}
Ok, now you've to create a interface to describe the structure of your state.
export interface AppModuleStateInterface {
todo: {
items: string[];
};
}
Before you can register your state to redux, you need to create a class that represents your state. This class is responsible to resolve the initial state.
You need to decorate your class by @ReduxState
and to provide a application wide unique state name
to it. If you can
not be sure that your name is unique enough, then you can add a unique id to it (as in the example shown below).
The @ReduxState
decorator is only valid for classes which implement the ReduxStateInterface
. This is an generic
interface where you've to provide your previously created AppModuleStateInterface
. The ReduxStateInterface
compels
you to implement a public method getInitialState
. This method is responsible to know, how the initial state can be
computed and will return it as an Promise
, Observable
or an implementation of the state interface directly.
Note: The method
getInitialState
is called by ngx-redux automatically! Your state will be registered to the root state after the initial state was resolved successfully.
import { ReduxState, ReduxStateInterface } from '@harmowatch/ngx-redux-core';
import { AppModuleStateInterface } from './app.module.state.interface';
@ReduxState({
name: 'app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72'
})
export class AppModuleState implements ReduxStateInterface<AppModuleStateInterface> {
getInitialState(): AppModuleStateInterface {
return {
todo: {
items: [ 'Item 1', 'Item 2' ],
}
};
}
}
import { ReduxState, ReduxStateInterface } from '@harmowatch/ngx-redux-core';
import { AppModuleStateInterface } from './app.module.state.interface';
@ReduxState({
name: 'app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72'
})
export class AppModuleState implements ReduxStateInterface<AppModuleStateInterface> {
getInitialState(): Promise<AppModuleStateInterface> {
return Promise.resolve({
todo: {
items: [ 'Item 1', 'Item 2' ],
}
});
}
}
Note: If you return a unresolved
Promise
your state is never registered!
import { ReduxState, ReduxStateInterface } from '@harmowatch/ngx-redux-core';
import { AppModuleStateInterface } from './app.module.state.interface';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
@ReduxState({
name: 'app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72'
})
export class AppModuleState implements ReduxStateInterface<AppModuleStateInterface> {
getInitialState(): Observable<AppModuleStateInterface> {
const subject = new BehaviorSubject<AppModuleStateInterface>({
todo: {
items: [ 'Item 1', 'Item 2' ],
}
});
subject.complete();
return subject.asObservable();
}
}
Note: If you return a uncompleted
Observable
your state is never registered!
The next thing you need to do, is to register your state. For that ngx-redux accepts a configuration property state
.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReduxModule } from '@harmowatch/ngx-redux-core';
import { AppComponent } from './app.component';
import { AppModuleState } from './app.module.state';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
ReduxModule.forRoot({
state: {
provider: AppModuleState,
}
}),
],
providers: [],
bootstrap: [ AppComponent ]
})
export class AppModule {
}
Note: For lazy loaded modules you've to use
forChild
.
Your redux module is ready to run now. Once your initial state was resolved, your redux module is registered to the global redux state like this:
{
"app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72": {
"todo": {
"items": [
"Item 1",
"Item 2"
]
}
}
}
To select values from the state you can choose between this three options:
- a Angular Pipe
- a Annotation
- a Class
Each selector will accept a relative todo/items or an absolute path /app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72/todo/items. It's recommended to use relative paths only. The absolute path is only there to give you a maximum of flexibility.
The easiest way to get values from the state, is to use the reduxSelect
pipe together with Angular's async
pipe. The
right state is determined automatically, because you're in a Angular context.
<pre>{{ 'todo/items' | reduxSelect | async | json }}</pre>
<pre>{{ '/app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72/todo/items' | reduxSelect | async | json }}</pre>
If you want to access the state values in your component you can use the @ReduxSelect
decorator. ngx-redux can not
determine which state you mean automatically, because decorators run outside the Angular context. For that you've to
pass in a reference to your state class as 2nd argument. When you specify an absolute path, you don't need the 2nd
argument anymore.
import { Component } from '@angular/core';
import { ReduxSelect } from '@harmowatch/ngx-redux-core';
import { AppModuleState } from './app.module.state';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
@ReduxSelect('todo/items', AppModuleState)
private todoItems: Observable<string[]>;
constructor() {
this.todoItems.subscribe((items) => console.log('ITEMS', items));
}
}
import { Component } from '@angular/core';
import { ReduxSelect } from '@harmowatch/ngx-redux-core';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
@ReduxSelect('/app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72/todo/items')
private todoItems: Observable<string[]>;
constructor() {
this.todoItems.subscribe((items) => console.log('ITEMS', items));
}
}
import { Component } from '@angular/core';
import { ReduxStateSelector } from '@harmowatch/ngx-redux-core';
import { AppModuleState } from './app.module.state';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
constructor() {
const selector = new ReduxStateSelector('todo/items', AppModuleState);
selector.getSubject().subscribe((items) => console.log('ITEMS', items));
// or
selector.getReplaySubject().subscribe((items) => console.log('ITEMS', items));
// or
selector.getObservable().subscribe((items) => console.log('ITEMS', items));
// or
selector.getBehaviorSubject([ 'Default Item' ]).subscribe((items) => console.log('ITEMS', items));
}
}
import { Component } from '@angular/core';
import { ReduxStateSelector } from '@harmowatch/ngx-redux-core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
constructor() {
const selector = new ReduxStateSelector('/app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72/todo/items');
selector.getSubject().subscribe((items) => console.log('ITEMS', items));
// or
selector.getReplaySubject().subscribe((items) => console.log('ITEMS', items));
// or
selector.getObservable().subscribe((items) => console.log('ITEMS', items));
// or
selector.getBehaviorSubject([ 'Default Item' ]).subscribe((items) => console.log('ITEMS', items));
}
}
6. Dispatch an Redux Action
To dispatch an action is very easy. Just annotate your class method by @ReduxAction
. Everytime your method is called
ngx-redux will dispatch a Redux Action for you automatically!
The return
value of the decorated method will become the payload of the action and the name of the method is used as
the action type.
Note: It's very useful to write a provider, where the action method(s) are delivered by. See the example below.
import { Injectable } from '@angular/core';
import { ReduxAction } from '@harmowatch/ngx-redux-core';
@Injectable()
export class AppActions {
@ReduxAction()
public addTodo(todo: string): string {
return todo;
}
}
Then register the provider to your module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReduxModule } from '@harmowatch/ngx-redux-core';
import { AppActions } from './app.actions'; // (1) Add the import
import { AppComponent } from './app.component';
import { AppModuleState } from './app.module.state';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
ReduxModule.forRoot({
state: {
provider: AppModuleState,
}
}),
],
providers: [ AppActions ], // (2) Add to the provider list
bootstrap: [ AppComponent ]
})
export class AppModule {
}
Now you can inject the provider to your component:
import { Component } from '@angular/core';
import { AppActions } from './app.actions';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
constructor(appActions: AppActions) {
appActions.addTodo('SampleTodo');
}
}
The example above will dispatch the following action:
{
"type": "addTodo",
"payload": "SampleTodo"
}
Ok that's cool, but there's no information in the action type that this was an AppActions
action, right?
But don't worry you can follow two different and very easy ways to fix that.
import { Injectable } from '@angular/core';
import { ReduxAction } from '@harmowatch/ngx-redux-core';
@Injectable()
export class AppActions {
@ReduxAction({
type: 'AppActions://addTodo'
})
public addTodo(todo: string): string {
return todo;
}
}
addTodo
will dispatch the following action from now on:
{
"type": "AppActions://addTodo",
"payload": "SampleTodo"
}
import { Injectable } from '@angular/core';
import { ReduxAction, ReduxActionContext } from '@harmowatch/ngx-redux-core';
@ReduxActionContext({
prefix: 'AppActions://'
})
@Injectable()
export class AppActions {
@ReduxAction()
public addTodo(todo: string): string {
return todo;
}
}
addTodo
will dispatch the following action from now on:
{
"type": "AppActions://addTodo",
"payload": "SomeTodo"
}
import { Injectable } from '@angular/core';
import { ReduxAction, ReduxActionContext } from '@harmowatch/ngx-redux-core';
@ReduxActionContext({
prefix: 'AppActions://'
})
@Injectable()
export class AppActions {
@ReduxAction({
type: 'add-todo'
})
public addTodo(todo: string): string {
return todo;
}
}
addTodo
will dispatch the following action from now on:
{
"payload": "SampleTodo",
"type": "AppActions://add-todo"
}
We have no way to manipulate the data that are stored in the Redux Store yet. For that we need a reducer.
import { ActionInterface, ReduxReducer } from '@harmowatch/ngx-redux-core';
import { AppActions } from './app.actions';
import { AppModuleStateInterface } from './app.module.state.interface';
export class AppModuleReducer {
@ReduxReducer(AppActions.prototype.addTodo)
static addTodo(state: AppModuleStateInterface, action: ActionInterface<string>) {
return {
...state,
todo : {
...state.todo,
items : state.todo.items.concat(action.payload)
}
}
}
}
The last thing you need to do, is to wire the reducer against the state:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReduxModule } from '@harmowatch/ngx-redux-core';
import { AppActions } from './app.actions';
import { AppComponent } from './app.component';
import { AppModuleReducer } from './app.module.reducer'; // (1) Add the import
import { AppModuleState } from './app.module.state';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
ReduxModule.forRoot({
state: {
provider: AppModuleState,
reducers: [ AppModuleReducer ] // (2) Register the reducer
}
}),
],
providers: [ AppActions ],
bootstrap: [ AppComponent ]
})
export class AppModule {
}
One of the principles of Redux is to change the state using pure functions, only. Unfortunately there is no typescript support to decorate pure functions right now. That's the reason why ngx-redux uses classes where the reducer functions are shipped by. To find a viable solution the reducer functions shall be written as static methods.