0.1.18 • Published 6 years ago

ngx-redux v0.1.18

Weekly downloads
-
License
MIT
Repository
-
Last release
6 years ago

@harmowatch/ngx-redux-core

Decorator driven redux integration for Angular 2+

What is Redux?

Redux is a popular and common approach to manage a application state. The three principles of redux are:

Read more about Redux

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:

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:

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.