ngx-redux v0.1.18
@harmowatch/ngx-redux-core
Decorator driven redux integration for Angular 2+
What is ...
What is Redux?
Redux is a popular and common approach to manage a application state. The three principles of redux are:
What is ngx-redux?
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
Installation
First you need to install
- "redux" as peer dependency
- the "@harmowatch/ngx-redux-core" package itself
npm install redux @harmowatch/ngx-redux-core --save
Usage
1. Import the root ReduxModule
:
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!
Example
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.
Example
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 {
}
2. Describe your state
Ok, now you've to create a interface to describe the structure of your state.
Example
export interface AppModuleStateInterface {
todo: {
items: string[];
};
}
3. Create a class representing your module state
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.
As you can see in the example below, the class ...
... is decorated by @ReduxState
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).
... implements ReduxStateInterface
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.
Example 1) Interface implementation
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' ],
}
};
}
}
Example 2) Promise
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!
Example 3) Observable
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!
4. Register the state:
The next thing you need to do, is to register your state. For that ngx-redux accepts a configuration property state
.
Example
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"
]
}
}
}
5. Select data from the state
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.
5.1 Using the reduxSelect
pipe
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.
Example 1) Relative path (recommended)
<pre>{{ 'todo/items' | reduxSelect | async | json }}</pre>
Example 2) Absolute path (avoid)
<pre>{{ '/app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72/todo/items' | reduxSelect | async | json }}</pre>
5.2 Using the @ReduxSelect
annotation
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.
Example 1) Relative path (recommended)
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));
}
}
Example 2) Absolute path (avoid)
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));
}
}
5.3 Using the ReduxStateSelector
class
Example 1) Relative path (recommended)
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));
}
}
Example 2) Absolute path (avoid)
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.
Example
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:
Example
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.
Example 1) Provide a action type
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"
}
Example 2) Provide a action context (recommended)
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"
}
Example 3) Combine the ReduxContext and action type
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"
}
7. Reduce the State
We have no way to manipulate the data that are stored in the Redux Store yet. For that we need a reducer.
Example
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:
Example
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 {
}
Known violations / conflicts
Redux principles
Changes are made with pure functions
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.