Redux and Component Architecture

In the above example, our counter component is a smart component. It knows about Redux, the structure of the state and the actions it needs to call. In theory you can drop this component into any area of your application and just let it work. But it will be tightly bound to that specific slice of state and those specific actions. For example, what if we wanted to have multiple counters tracking different things on the page? For example, counting the number of red clicks vs blue clicks.

To help make components more generic and reusable, it's worth trying to separate them into container components and presentational components.

Container Components Presentational Components
Location Top level, route handlers Middle and leaf components
Aware of Redux Yes No
To read data Subscribe to Redux state Read state from @Input properties
To change data Dispatch Redux actions Invoke callbacks from @Output properties

redux docs

Keeping this in mind, let's refactor our counter to be a presentational component. First, let's modify our app-container to have two counter components on it as we currently have it.

import { Component } from '@angular/core';

@Component({
  selector: 'simple-redux'
  template: `
    <div>
      <h1>Redux: Two components, one state.</h1>
      <div style="float: left; border: 1px solid red;">
        <h2>Click Counter</h2>
        <counter></counter>
      </div>
      <div style="float: left; border: 1px solid blue;">
        <h2>Curse Counter</h2>
        <counter></counter>
      </div>
    </div>
  `
})
export class SimpleRedux {}

View Example

As you can see in the example, when clicking on the buttons the numbers in both components will update in sync. This is because the counter component is coupled to a specific piece of state and action.

Looking at the example, you can see that there is already an app/reducers/curse-reducer.ts and app/actions-curse-actions.ts. They are pretty much the same as the counter actions and counter reducer, we just wanted to create a new reducer to hold the state of it.

To turn the counter component from a smart component into a dumb component, we need to change it to have data and callbacks passed down into it. For this, we will pass the data into the component using @Input properties, and the action callbacks as @Output properties.

We now have a nicely-reusable presentational component with no knowledge of Redux or our application state.

app/components/counter-component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
  selector: 'counter',
  template: `
  <p>
    Clicked: {{ counter | async }} times
    <button (click)="increment.emit()">+</button>
    <button (click)="decrement.emit()">-</button>
    <button (click)="incrementIfOdd.emit()">Increment if odd</button>
    <button (click)="incrementAsync.emit()">Increment async</button>
  </p>
  `
})
export class Counter {
  @Input() counter: Observable<number>;
  @Output() increment = new EventEmitter<void>();
  @Output() decrement = new EventEmitter<void>();
  @Output() incrementIfOdd = new EventEmitter<void>();
  @Output() incrementAsync = new EventEmitter<void>();
}

Next, let's modify the main app container to hook up these inputs and outputs to the template.

@Component app/src/containers/app-containter.ts

@Component({
  selector: 'simple-redux',
  providers: [ CounterActions, CurseActions ],
  template: `
  <div>
    <h1>Redux: Presentational Counters</h1>
    <div style="float: left; border: 1px solid red;">
      <h2>Click Counter</h2>
      <counter [counter]="counter$"
          (increment)="counterActions.increment()"
          (decrement)="counterActions.decrement()"
          (incrementIfOdd)="counterActions.incrementIfOdd()"
          (incrementAsync)="counterActions.incrementAsync()">
      </counter>
    </div>
    <div style="float: left; border: 1px solid blue;">
      <h2>Curse Counter</h2>
      <counter [counter]="curse$"
          (increment)="curseActions.castCurse()"
          (decrement)="curseActions.removeCurse()"
          (incrementIfOdd)="curseActions.castIfOdd()"
          (incrementAsync)="curseActions.castAsync()">
      </counter>
    </div>
  </div>
    `
})

At this point, the template is attempting to call actions on our two ActionCreatorServices, CounterActions and CurseActions; we just need to hook those up using Dependency Injection:

app/src/containers/app-container.ts

import { Component, View, Inject, OnDestroy, OnInit } from '@angular/core';
import { select } from 'ng2-redux';
import { Observable } from 'rxjs';
import { CounterActions } from '../actions/counter-actions';
import { CurseActions } from '../actions/curse-actions';

@Component({ /* see above .... */})
export class SimpleRedux {
  @select() counter$: Observable<number>;
  @select() curse$: Observable<number>;

  constructor(
    public counterActions: CounterActions,
    public curseActions: CurseActions) {
    }
}

View Ng2-Redux Example View Ngrx Example

Our two Observables, counter$ and curse$, will now get updated with a new value every time the relevant store properties are updated by the rest of the system.

results matching ""

    No results matching ""