20 May 2024
5 min

Change Detection Big Picture – Unidirectional data flow

Unidirectional data flow

Angular enforces so-called unidirectional data flow from top to bottom.
The essence of this convention is that data flows from the parent to the child, but not the other way round.
If the parent state changes, and there are input bindings to child components,
those changes are pushed down to the child component during the rendering (change detection) process.

The most common way for components to communicate with each other is through bindings.
If you have a parent component A defined like this in Angular,
you can see that it has a child component B and the parent A component
passes the value to the child B component through obj input binding:

// parent component
@Component({
    template: `<b-component [prop]="value"></b-component>`
})
export class A {
    value = {name: 'initial'};
}

// child component
@Component({ ... })
export class B {
    @Input() prop;
}

It’s super important to understand that Angular update bindings during change detection.
So when the framework runs change detection for the parent component A it will update the prop input
binding on the child component B. This means that change detection is also always performed from top to bottom
for every single component, every single time, starting from the root component.

If the child state changes and the parent somehow depends on those changes,
the child needs to explicitly send an event back up to the parent with the changed data.
There’s no built-in mechanism that tracks those changes in the child component and auto-magically propagates
them back to the parent.

This is what is called unidirectional data flow. Application state gets stable after a single pass of change detection.
This is a lot more efficient and predictable than cycles, which would be the case if children could update its parents.
We always know where the data we use in our views comes from, because it can only result from its parent component.

To understand this restriction better, imagine that during the current change detection run some properties
of already checked components somehow got updated. As a result, expressions in templates will produce new values
that are inconsistent with what Angular rendered on the screen as part of those components check.
What does Angular have to do to bring the application state and screen in sync?
It certainly could run another change detection cycle in an attempt to synchronize those.
But what if during that process some properties are updated again? You should see the pattern now.
Angular could actually end up in an infinite loop of change detection runs trying to bring in sync
the application state and the results of side effects, e.g. what’s rendered on the screen.
So, to summarize, once Angular has processed bindings for the current component,
you can no longer update the properties of the component that are used in expressions for bindings.

Enforcing unidirectional flow

In the development mode Angular enforces unidirectional data flow by running an extra check
after regular change detection cycle.
This check includes comparing the current values of component properties and expressions in the template to the ones
Angular used and remembered during the preceding change detection cycle. If any of the values are different,
the framework throws an infamous error ExpressionChangedAfterItHasBeenCheckedError.

Image alt

It’s easy to observe that effect by updating the state of a parent component from inside the AfterViewChecked hook
in a child component. Just make sure the property of an ancestor component being updated leads to side effects,
i.e. DOM updates or child component input bindings updates.

// parent component
@Component({
	selector: "a-cmp",
	template: `<b-cmp [prop]="value"></b-cmp>`,
})
export class A {
	value = { name: "initial" };
}

// child component
@Component({
	selector: "b-cmp",
	template: `{{ prop.name }}`,
})
export class B {
	@Input() prop;

	constructor(private parent: A) {}

	ngAfterViewChecked() {
		this.parent.value.name = "updated";
	}
}

Check the live running example here.

In production mode, Angular doesn’t throw the error, but it also doesn’t try to stabilize the application state.
This often leads to inconsistencies between the values of the component properties and what’s rendered on the screen.
During the next change detection the inconsistency might be resolved, e.g. the updated property synced to the DOM.
However, if the property value is again updated after the change detection has checked the component,
the inconsistent state may never be resolved. We’ll spend a lot of time explaining this error when
we’ll be reviewing change detection operations.

Although there’s no built-in mechanism in Angular that can cause parent component model update during change detection
it’s still possible to do cause that affect unintentionally through a variety of mechanisms: injecting parent component reference,
a shared service or synchronous event broadcasting.

Child to parent communication

In Angular, the notification mechanism from a child component to its parent is implemented through output bindings,
often referred to as component events. It looks like this:

// parent component
@Component({
    template: `
        <h1>Hello {{value.name}}</h1>
        <a-comp (updateObj)="value = $event"></a-comp>
    `
})
export class AppComponent {
    value = {name: 'initial'};

    constructor() {
        setTimeout(() => {
            console.log(this.value); // logs {name: 'updated'}
        }, 3000);
    }
}

// child component
@Component({...})
export class AComponent {
    @Output() updateObj = new EventEmitter();

    constructor() {
        setTimeout(() => {
            this.updateObj.emit({name: 'updated'});
        }, 2000);
    }
}

Component evens are most often broadcasted from within the event handlers which are attached to UI events,
network events or timers. Since those events are triggered before Angular runs the change detection cycle,
it’s perfectly fine to emit an event that will result to a parent component update.
In fact, those browser events is what most often triggers change detection.
The assumption is that event handlers might change the application state so Angular needs to process side effects,
like synchronizing component state with the DOM.

However, if the emitted event leads to updates of a parent component properties,
and those properties are processed during change detection, i.e. template expressions,
the event must be emitted outside of the Angular’s change detection loop.
Otherwise this will lead to the properties of a parent component being updated inside
the change detection cycle leading to the ExpressionChangedAfterItHasBeenCheckedError error.

Sometimes the event is emitted synchronously from inside the handler that is being executed during the change detection loop.
It’s OK as long the logic that reacts to the event updates the properties of a parent component
before Angular completes the check of the parent component.
This can be done, for example, from the DoCheck hook,
which is executed on the child component before parent component changes are processed.

We’ll take a closer look at this case in the corresponding chapter.

One-way data flow in global state management libs

Most web applications today use state management libraries like NgRx and Redux.
These library implement functionality related to processing and storing business relevant data.
Today this service layer is known as state management.
The libraries that operate in this layer are most often not closely tied to the presentation layer with components
that display application relevant data to a user through DOM. Hence it makes sense to distinguish between
unidirectional data flow enforced during change detection and the one way data flow that’s used as a primary
architecture principle for state management libs.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.