To define your application state, use an interface called AppState or IAppState, depending on the naming conventions used on your project.
- const loginSendAction: Action = createAction('LOGIN_SEND', {
- username: 'katie',
- password: '35c0cd1ecbbb68c75498b83c4e79fe2b'
- });
Modifying Application State by Dispatching Actions
Most Redux apps have a set of functions, called "action creators", that are used to set up and dispatch actions. In Angular, it's convenient to define your action creators as @Injectable() services, decoupling the dispatch, creation and side-effect logic from the @Component classes in your application.
Synchronous Actions - Here is a simple example.
app/store/counter/counter.actions.ts
- import {Injectable} from '@angular/core';
- import {Store} from '@ngrx/store';
-
- import {createAction} from '../createAction';
- import {AppState} from '../../models/appState';
-
- @Injectable()
- export class CounterActions {
- static INCREMENT = 'INCREMENT';
- static DECREMENT = 'DECREMENT';
- static RESET = 'RESET';
- constructor(private store: Store<AppState>) {
- }
-
- increment() {
- this.store.dispatch(createAction(CounterActions.INCREMENT));
- }
-
- decrement() {
- this.store.dispatch(createAction(CounterActions.DECREMENT));
- }
-
- reset() {
- this.store.dispatch(createAction(CounterActions.RESET));
- }
- }
As you can see, the action creators are simple functions that dispatch Action objects containing more information that describes the state modification.
Asynchronous Actions
This "ActionCreatorService" pattern comes in handy if you must handle asynchronous or conditional actions (users of react-redux may recognize this pattern as analogous to redux-thunk in a dependency-injected world).
app/store/counter/counter.actions.ts
- import {Injectable} from '@angular/core';
- import {Store} from '@ngrx/store';
- import {createAction} from '../createAction';
- import {AppState} from '../../models/appState';
-
- @Injectable()
- export class CounterActions {
-
- constructor(private store: Store<AppState>) {
-
- }
-
- incrementIfOdd() {
- this.store.select(appState => appState.counter.currentValue)
- .take(1)
- .subscribe(currentValue => {
- if (currentValue % 2 !== 0) {
- this.store.dispatch(createAction(CounterActions.INCREMENT);
- }
- });
- }
-
- incrementAsync(timeInMs: number = 1000) {
- this.delay(timeInMs).then(() => this.store.dispatch(createAction(CounterActions.INCREMENT)));
- }
-
- private delay(timeInMs: number) {
- return new Promise((resolve) => {
- setTimeout(() => resolve() , timeInMs);
- });
- }
- }
- import {Injectable} from '@angular/core';
- import {Store} from '@ngrx/store';
-
- import {createAction} from '../createAction';
- import {AppState} from '../../models/appState';
-
- @Injectable()
- export class SessionActions {
- static LOGIN_USER_PENDING = 'LOGIN_USER_PENDING';
- static LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';
- static LOGIN_USER_ERROR = 'LOGIN_USER_ERROR';
- static LOGOUT_USER = 'LOGOUT_USER';
-
- constructor(
- private store: Store<AppState>,
- private authService: AuthService
- ) {
- }
-
- loginUser(credentials: any) {
- this.store.dispatch(createAction(SessionActions.LOGIN_USER_PENDING));
- this.authService.login(credentials.username, credentials.password)
- .then(result => this.store.dispatch(createAction(SessionActions.LOGIN_USER_SUCCESS, result)))
- .catch(() => this.store.dispatch(createAction(SessionActions.LOGIN_USER_ERROR)));
- };
-
- logoutUser() {
- this.store.dispatch(createAction(SessionActions.LOGOUT_USER));
- };
- }
Review of Reducers and Pure Functions
One of the core concepts of Redux is the reducer. A reducer is a function with the signature (accumulator: T, item: U) => T. Reducers are often used in JavaScript through the Array.reducemethod, which iterates over each of the array's items and accumulates a single value as a result. Reducers should be pure functions, meaning they don't generate any side-effects. A simple example of a reducer is the sum function,
let x = [1, 2, 3].reduce((sum, number) => sum + number, 0);
Reducers as State Management
Reducers are simple ideas that turn out to be very powerful. With Redux, you replay a series of actions into the reducer and get your new application state as a result. Reducers in a Redux application should not mutate the state, but return a copy of it, and be side-effect free. This encourages you to think of your application as UI that gets "computed" from a series of actions in time.
Simple Reducer - Let's take a look at a simple counter reducer.
app/store/counter/counter.reducer.ts
- import {Action} from '@ngrx/store';
- import {CounterActions} from './counter.actions';
-
- export default function counterReducer(state: number = 0, action: Action): number {
- switch (action.type) {
- case CounterActions.INCREMENT:
- return state + 1;
- case CounterActions.DECREMENT:
- return state - 1;
- case CounterActions.RESET:
- return 0;
- default:
- return state;
- }
- }
Complex Reducer
Another consideration when creating your reducers is to ensure that they are immutable and not modifying the state of your application. If you mutate your application state, it can cause unexpected behavior. There are a few ways to help maintain immutability in your reducers. One way is by using new ES6 features such as Object.assign() or the spread operator for arrays.
app/models/counter.ts
- export function setCounterCurrentValue(counter: Counter, currentValue: number): Counter {
- return Object.assign({}, counter, { currentValue });
- }
- import {Action} from '@ngrx/store';
- import {Counter, createDefaultCounter, setCounterCurrentValue} from '../../models/counter';
- import {CounterActions} from './counter.actions';
-
- export function counterReducer(
- counter: Counter = { currentValue: 0 },
- action: Action
- ): Counter {
- switch (action.type) {
- case CounterActions.INCREMENT:
- return setCounterCurrentValue(counter, counter.currentValue + 1);
-
- case CounterActions.DECREMENT:
- return setCounterCurrentValue(counter, counter.currentValue - 1);
-
- case CounterActions.RESET:
- return setCounterCurrentValue(counter, 0);
-
- default:
- return counter;
- }
- }
- import {counterReducer} from './counter/counter.reducer';
-
- export const rootReducer = {
- counter: counterReducer
- };
- import {BrowserModule} from '@angular/platform-browser';
- import {NgModule} from '@angular/core';
- import {FormsModule} from '@angular/forms';
- import {HttpModule} from '@angular/http';
- import {StoreModule} from '@ngrx/store';
- import {EffectsModule} from '@ngrx/effects';
- import 'rxjs/Rx';
- import {rootReducer} from './store/rootReducer';import {CounterActions} from './store/actions';
- import {CounterEffects} from './store/effects';
- import {AppComponent, CounterComponent} from './components';
- import {CounterService} from './services';
-
- @NgModule({
- imports: [
- BrowserModule,
- FormsModule,
- HttpModule,
- StoreModule.provideStore(rootReducer)
- ],
- declarations: [
- AppComponent,
- CounterComponent
- ],
- providers: [
- CounterActions,
- CounterService
- ],
- bootstrap: [AppComponent]
- })
- export class AppModule {
-
- }
- import {Component, Input} from '@angular/core';
- import {Observable} from 'rxjs/Observable';
- import {CounterService} from '../services';
- import {CounterActions} from '../store/counter/counter.actions';
-
- @Component({
- selector: 'counter',
- templateUrl: './counter.component.html'
- })
- export class CounterComponent {
- private currentValue$: Observable<number>;
- constructor(
- counterService: CounterService,
- public actions: CounterActions
- ) {
- this.currentValue$ = counterService.getCurrentValue();
- }
- }
- <p>
- Clicked: {{currentValue$ | async}} times
- <button (click)="actions.increment()">+</button>
- <button (click)="actions.decrement()">-</button>
- <button (click)="actions.reset()">Reset</button>
- </p>
- import {Component} from '@angular/core';
- import {Observable} from 'rxjs';
-
- import {Counter} from '../../models/counter';
- import {CounterService} from '../../services/counter.service';
- import {CounterActions} from '../../store/counter/counter.actions';
-
- @Component({
- selector: 'app-root',
- templateUrl: './app.component.html',
- styleUrls: ['./app.component.css']
- })
- export class AppComponent {
-
- counter$: Observable<Counter>;
-
- constructor(
- counterService: CounterService,
- public actions: CounterActions
- ) {
- this.counter$ = counterService.getCounter();
- }
-
- }
- <counter [counter]="counter$ | async"
- (onIncrement)="actions.increment()"
- (onDecrement)="actions.decrement()"
- (onReset)="actions.reset()">
- </counter>
- import {Component, Input, EventEmitter, Output} from '@angular/core';
-
- import {Counter} from '../../models/counter';
-
- @Component({
- selector: 'counter',
- templateUrl: './counter.component.html'
- })
- export class CounterComponent {
-
- @Input()
- counter: Counter;
-
- @Output()
- onIncrement: EventEmitter<void> = new EventEmitter<void>();
-
- @Output()
- onDecrement: EventEmitter<void> = new EventEmitter<void>();
-
- @Output()
- onReset: EventEmitter<void> = new EventEmitter<void>();
-
- }
- <p>
- Clicked: {{counter.currentValue}} times
- <button (click)="onIncrement.emit()">+</button>
- <button (click)="onDecrement.emit()">-</button>
- <button (click)="onReset.emit()">Reset</button>
- </p>