avatar

Derek Zeng

A programmer

Angular data model and Redux

by coderek

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 built on top of other components. Descendent component 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 every event frame. 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.

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();
    }
}

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.

@Output should be generated only when it potentially affected some of the input values.

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 simlilar 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)