Blog

Are model inputs a bad practice?

Angular
State management
Are model inputs a bad practice?

Should We Go With the Flow?

As frontend developers, a big part of our job is managing the state of the application. This can be quite challenging, especially as the application grows in size and complexity. To make it more manageable, I have always tried to keep data flow unidirectional, meaning data flows down the component tree. This approach makes it easier to reason about the state of the application and how it changes over time.

In some frameworks, like React, unidirectional data flow is the mandatory way of working. But in Angular, you have the flexibility to also use bidirectional data flow, meaning data can also flow up the component tree.

I’ve always been a bit hesitant to use this bidirectional approach, but is this hesitation justified? Angular even added the model inputs api after the introduction of signals, which proves that the Angular team is supporting the idea of bidirectional data flow.

The Angular Way

In Angular, there are two primary ways to pass data directly between components:

  1. Unidirectional Flow: Using (input) and (output) properties, data flows from parent to child via (input) and events from child to parent via (output).

  2. Bidirectional Flow: In Angular you can use two-way data binding, known as "banana in the box" syntax [(model)] that combines (input) and (output) into a single binding. This allows data to flow both ways between a parent and child component with minimal boilerplate code.

The most common way Bidirectional data flow is used in Angular is with forms. When you bind a form control to a model using [(ngModel)], you are using bidirectional data flow. But you can also use this syntax with custom properties in your components and with the new model inputs.

@Component({
  selector: 'app-parent',
  imports: [ChildComponent],
  template: `
    <main>
      <app-child [(title)]="initialTitle"></app-child>
    </main>
  `
})
export class ParentComponent {
  initialTitle = 'Title';
}

// With the model inputs api it is easy to use bidirectional data flow in Angular.
@Component()
export class ChildComponent {
  title = model<string>();

  updateTitle(newTitle: string): void {
    this.title.set(newTitle);
  }
}

The Case for Bidirectional Data Flow

Two way data binding can make code more readable and reduce boilerplate in certain scenarios. For instance, when dealing with forms that have multiple input fields, it can be cumbersome to pass the values of these fields up the component tree using (output) events. In such cases, [(model)] simplifies the implementation:

Efficiency: Instead of managing multiple event bindings and explicit state updates, you bind the input fields directly to a model in the parent component.
Readability: The code becomes cleaner, as you avoid the overhead of manually managing communication between parent and child components.

This approach allows for seamless two-way synchronization between the UI and the data model, which can be particularly useful in scenarios like dynamic forms or small interactive components.

So this seems really nice, but why don't we use bidirectional data flow all the time?

The Case Against Bidirectional Data Flow

The main argument against bidirectional data flow is that it complicates reasoning about the application state. When data flows in both directions, it becomes harder to track where state originates and how it propagates. Debugging and state management can quickly grow challenging.

I think that especially in two cases bidirectional data flow can be problematic:

  1. When other state depends on the state you want to pass.
  2. When the state you want to pass is used by different components, other then the parent.

Let's look at an example to illustrate this point:

Imagine a dashboard with:

A Filter Panel to select filters like date range and region. Chart, Table, and Summary components displaying data based on the filters. In the summary component, certain actions can be done when values derived from the filters are below a threshold. With two-way binding [(filters)], any filter change in one component updates the parent and propagates to others.

Mermaid diagram Mermaid diagram

This seems convenient but leads to issues:

  • Debugging Chaos: Changes in the Table trigger updates in the parent, affecting the Chart and Summary, making it hard to trace the source of changes.
  • Circular Updates: Filters modified in the Chart can update the Table, which then updates the parent, creating confusing loops.
  • Scaling Problems: Adding new components increases complexity, as every component can modify the shared state.

Why Unidirectional Flow is Better in this case: A central state (e.g., a store) ensures predictable updates: components subscribe to the state and dispatch changes, avoiding conflicts and maintaining clarity.

Conclusion

Ultimately, the choice between unidirectional and bidirectional data flow depends on the specific use case. While bidirectional data flow can improve readability and reduce boilerplate in certain scenarios, unidirectional data flow remains the safer and more predictable choice for most applications. It provides a clear understanding of how data flows through the system, which is invaluable in larger and more complex applications.

Where two-way binding really shines is when there is a 1:1 relationship between a data point and a single element in your UI that displays and modifies that state. For example the use of [(ngModel)] is a perfect example, but when this assumption breaks down, you can quickly run into issues with two-way binding.

So, are model inputs a bad practice? Not really. Just like any tool, they have their time and place. Use them wisely, and they can become a valuable part of your Angular toolkit.

🎶 Bonus: did you get the song reference?

The image and subtitle of this article are inspired by the song "Go With the Flow" by Queens of the Stone Age. I highly recommend giving it a listen!

Queens of the Stone Age - Go with the flow