avatar

Derek Zeng

A programmer

Angular data model and Redux

We know that Angular uses component based architecture. Each component is an independent piece of UI that consists of its own state management, styles and templates. A component can be composed of other components. Descendent components may inherit styles from ancestor component due to the use of shadow DOM.

The key idea is simple. We should keep the component as independent as possible, like pure functions, so that it can be composed, reused and tested easily.

Each component despite having private states, it can declare inputs and outputs. Both inputs and outputs can be communicated through parent component as well as service injection.

E.g.

export class TimerComponent {
    // attribute getter that proxies to service
    // it will detect change every time when service value changes
    get timeNow() {
        return radioService.timeNow;
    }
    constructor(private radioService: RadioService) {}
} 

Rendered template should always be in sync with the private state of the component controller.

For the example above, the change of radioService.timeNow will cause the UI to be updated and show the correct time.

There are only a handful of things that can affect private state of a component.

  1. change of inputs
  2. user interaction

If the private state of a component and its decendants only depends on their inputs, Angular provides an optimization option to skip all the change detection for itself and descendants unless the source input changed. It's well discussed here by former Angular team member Victor Savkin. To use this option, just specify change detection strategy.

@Component({
  template: '...',
  changeDetection: ChangeDetectionStrategy.OnPush
})
class TimeComponent {
  @Input() time;
}

In Angular change detection, every component has an change detector and they are run in the end of every event thread (I'll devote another post to talk about how angular decide to trigger change detection). E.g. UI events, ajax events, websocket events, setTimeout events etc. These are managed by so-called zones. Change detector for simple objects and primitive values compares the changes by value. Meaning that, if a=1 previously and a=2 now, it will mark the state dirty and the UI will be updated later. It also means object change b={time:100} then b['time']=121 won't trigger any change. To remedy this, we can use immutable.js.

export class TimerComponent {
    state = Immutable.fromJS({
        started: false,
        color: 'green'
    });

    changeColor() {
        this.state = this.state.set('color', 'red'); // trigger a change
    }
}

If the app uses immutable data type throughout, all the non-interactive component (e.g. reporting table) can be optimized. Some people think immutable js is too verbose and the API is unfriendly, they rather use the following form:

...
return Object.assign({}, state, {change: 123});
...

This is also valid because the state is re-assigned to a new object, thus considered changed. The only downside of it is when the state object is complicated, this form can be complicated too.

We also can use Observables as source of changes. Angular knows how to subscribe it and listen for changes.

@Component({
    template: '{{programMeta?.name | async}}'
})
export class AppComponent {
    programMeta = null;
    constructor(private radioService: RadioService) {
        this.programMeta = radioService.getProgramMetaObservable();
    }
}

The async pipe in the template says the programMeta is an observable.

Note the data flow here. Since we can pass input from dependency injection, we can also send messages directly to injected services. This process of generating output should not affect the private state of component. When the message is received by the service, it should update the state of its own and then propagate to all component that uses the value.

So the changes is modelled as a stream of events, and the events flow back to the source and dispatched to subscribers as their stream of events. Everything seems to be reactive now. There are only observers and subscribers in the system. And most subscriber is also an observer.

@Output in Angular is just one form of stream binding that make the component less dependent on this shared service.

Using service injection to manage global states seems to be a viable solution for large projects.

I was reading the complete redux book recently. The idea of redux is surprisingly similar to what we have here. Global state management with uni-directional data flow. Every change to the global state produces a new instance of global state. All listeners to the changes get notified when it happens. Sources of changes are predictable since all of them have to go through reducers (redux equivalent of controller).

In Angular, global states are managed by shared services. Global states affect components' private states. Components are listening to the global state change from @Inputs. Uni-directional data flow manifests in the form of events output by components and eventually consumed by shared services and changes broadcast to all listeners.

So, do we need redux to work with Angular? Probably not. Redux's idea is simple. As long as you have understood it, it's okay to come up with your own shared service architecture. It's not necessary to include redux styles even if it's not harmful at all. Redux's functional styles could interfere with Angular's classical style after all.

(End of article)