DEV Community

Cover image for Angular in React Terms: Component State
Gleb Irovich
Gleb Irovich

Posted on

Angular in React Terms: Component State

In the first episode of this series, we have looked into some basics of React and Angular. Check it out here if you haven't read it yet.

In today's episode I would like to talk about the thing which is a core element of any frontend framework, the thing, that, probably, makes those frameworks so powerful and popular. This thing is the State. The state allows components to be self-contained building blocks, state stores information about changes in the UI, state encapsulates business logic of the application.

What is the State?

Back then, in the good old days, when web sites were powered mainly by plain JS, the state was a variable that kept a piece of information about the user's journey on the page. Variables had some limitations. Since they were mainly defined in the global scope, they could have been mutated by accident or even accessed by the client via console.
Over time the concept has evolved, application state was moved to the closures, where it was perfectly encapsulated inside a function scope, and then to the private fields of the classes.
Modern frontend frameworks are chasing exactly the same goal. They are trying to keep the information encapsulated as close to the place where it's used as possible. The component state, in the end, defines how the view will look like and what will be the reaction to the next user's interaction.

State in Vanilla JS

Before we start talking about state management in React and Angular, let's think for a second what the state would be if we implemented it using Vanilla JS. From my standpoint, the concept of the component state is very similar to the commonly used module pattern is JS.

const statefulModule = (function(initialState = {}) {
  let state = {
    count: 0,
    ...initialState
  };

  const setState = (stateUpdate) => {
    state = {...state, ...stateUpdate};
  }

  const getState = () => state;

  return [getState, setState];
})()

const [getState, setState] = statefulModule;

setState({count: 1});
getState(); // Output: {count: 1}
Enter fullscreen mode Exit fullscreen mode

IEFE creates a closure, so that the state variable is kept private, however, methods to update state and get its latest value are exposed to the module consumer.
An important point to notice: in this example, we maintain state immutability. Whenever the state is updated function returns a reference to a new JS object, which can easily be validated by a simple test.

getState() === getState() // Output true
const prevState = getState()
setState({count: 2})
prevState === getState() // Output false
Enter fullscreen mode Exit fullscreen mode

So why is immutability important? At the end of the day any framework persuades a goal of responding to user's interactions. In order to render the latest available information framework needs to know if the change actually happened. State immutability allows efficient change detection which in turn triggers view update.

Stateful and Stateless Components

Perhaps, another important thing to highlight before we dive deeper into the comparison between state management in React and Angular is what we can actually count as a stateful component.

// React
const CounterDisplayComponent = ({value}) => {
  return <strong>{value}</strong>
}

const CounterButtonComponent = ({children}) => {
  return <button>{children}</button>
}

const CounterComponent = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <CounterDisplayComponent value={count}/>
      <div>
        <CounterButtonComponent>Click me</CounterButtonComponent>
      </div>
    </div>
  )
}

export default CounterComponent;
Enter fullscreen mode Exit fullscreen mode
// Angular
@Component({
  selector: 'counter-display',
  template: `<strong>{{ value }}</strong>`,
})
export class CounterDisplayComponent {
  @Input() value: number;
}

@Component({
  selector: 'counter-button',
  template: `
    <button (click)="onButtonClick()">
      <ng-content></ng-content>
    </button>
  `,
})
export class CounterButtonComponent {
  @Output() buttonClicked = new EventEmitter<void>();
  onButtonClick() {
    this.buttonClicked.emit();
  }
}

@Component({
  selector: 'counter',
  template: `
    <counter-display [value]="count"></counter-display>
    <div>
      <counter-button>Click me</counter-button>
    </div>
  `,
})
export class CounterComponent {
  count = 0;
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we have a counter implemented in React and Angular. CounterComponent is our stateful component, because it contains information about the current count.
In case of React you might notice useState function, which is a React State Hook used to initialise state in the component with 0 as its initial value. In Angular we store count as a variable of the CounterComponent class.

Updating Component State

In functional React components useState hook returns an array, where the first element is the latest state value and the second is a function, that must be invoked to update the state. You should never modify React state directly because of the immutability concerns, which we discussed at the beginning of this post.

In Angular, since our state is just a class variable, we need to create a method to update it ourselves.

Let's add increment and decrement functionality to the counter.

// React
const CounterDisplayComponent = ({value}) => {
  return <strong>{value}</strong>
}

const CounterButtonComponent = ({children, onClick}) => {
  return <button onClick={onClick}>{children}</button>
}

const CounterComponent = () => {
  const [count, setCount] = useState(0);
  // Callbacks are passed down as props
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1)
  return (
    <div>
      <CounterDisplayComponent value={count}/>
      <div>
        <CounterButtonComponent onClick={increment}>+ Increment</CounterButtonComponent>
        <CounterButtonComponent onClick={decrement}>- Decrement</CounterButtonComponent>
      </div>
    </div>
  )
}

export default CounterComponent;
Enter fullscreen mode Exit fullscreen mode
// Angular
@Component({
  selector: 'counter-display',
  template: `<strong>{{ value }}</strong>`,
})
export class CounterDisplayComponent {
  @Input() value: number;
}

@Component({
  selector: 'counter-button',
  template: `
    <button (click)="onButtonClick()">
      <ng-content></ng-content>
    </button>
  `,
})
export class CounterButtonComponent {
  @Output() buttonClicked = new EventEmitter<void>();
  onButtonClick() {
    this.buttonClicked.emit();
  }
}

@Component({
  selector: 'counter',
  template: `
    <counter-display [value]="count"></counter-display>
    <div>
      <counter-button (buttonClicked)="increment()">+ Increment</counter-button>
      <counter-button (buttonClicked)="decrement()">- Decrement</counter-button>
    </div>
  `,
})
export class CounterComponent {
  count = 0;
    // Callbacks triggered on the event emited from the button component.
  increment() {
    this.count++;
  }
  decrement() {
    this.count--;
  }
}
Enter fullscreen mode Exit fullscreen mode

Try clicking the buttons now. We can see, that our counter value is updated in response to our interaction in the browser.

Async State Updates

It's often the case, that we have to update component state asynchronously. Think of timeouts, intervals, or data fetching. For the sake of example let's extend CounterComponent functionality and add an interval which will increment the count every 2 seconds.

React offers useEffects, a special hook which is useful for executing side effects and is meant to substitute component lifecycle methods available in class-based React components. useEffects accepts a callback and an array of dependencies that are required for caching. The callback is only triggered if values of dependencies array change.

In Angular components it's suggested to use lifecycle method OnInit and OnDestroy to set interval and clear existing interval respectively.

// React
const CounterDisplayComponent = ({value}) => {
  return <strong>{value}</strong>
}

const CounterButtonComponent = ({children, onClick}) => {
  return <button onClick={onClick}>{children}</button>
}

const CounterComponent = () => {
  const [count, setCount] = useState(0);
  // Callbacks are passed down as props
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1)

  useEffect(() => {
    // Reference to the interval is required to cancel it on component destroy.
    const interval = setInterval(() => setCount((currentCount) => currentCount + 1), 2000)
    // Returned function is executed if dependencies array changes or if component is destroyed.
    return () => clearInterval(interval)
  },[setCount]);

  return (
    <div>
      <CounterDisplayComponent value={count}/>
      <div>
        <CounterButtonComponent onClick={increment}>+ Increment</CounterButtonComponent>
        <CounterButtonComponent onClick={decrement}>- Decrement</CounterButtonComponent>
      </div>
    </div>
  )
}

export default CounterComponent;
Enter fullscreen mode Exit fullscreen mode

You can also notice that setCount can alternatively accept a callback, which is invoked with the current state and must return updated state value.

// Angular
@Component({
  selector: 'counter-display',
  template: `<strong>{{ value }}</strong>`,
})
export class CounterDisplayComponent {
  @Input() value: number;
}

@Component({
  selector: 'counter-button',
  template: `
    <button (click)="onButtonClick()">
      <ng-content></ng-content>
    </button>
  `,
})
export class CounterButtonComponent {
  @Output() buttonClicked = new EventEmitter<void>();
  onButtonClick() {
    this.buttonClicked.emit();
  }
}

@Component({
  selector: 'counter',
  template: `
    <counter-display [value]="count"></counter-display>
    <div>
      <counter-button (buttonClicked)="increment()">+ Increment</counter-button>
      <counter-button (buttonClicked)="decrement()">- Decrement</counter-button>
    </div>
  `,
})
export class CounterComponent implements OnInit, OnDestroy {
  count = 0;
  interval;

  ngOnInit() {
    // Reference to the interval is required to cancel it on component destroy.
    this.interval = setInterval(() => this.increment(), 2000);
  }

  ngOnDestroy() {
    if (this.interval) {
      clearInterval(this.interval);
    }
  }

  increment() {
    this.count++;
  }
  decrement() {
    this.count--;
  }
}
Enter fullscreen mode Exit fullscreen mode

Angular ChangeDetectionStrategy

Although ChangeDetectionStrategy deserves an episode on its own, I still would like to scratch a surface of that topic in regards to the component state. If you changed the changeDetectionSteategy of the CounterComponent you would notice, that our async state update doesn't have any effect on the view anymore.

@Component({
  selector: 'counter',
  template: `
    <counter-display [value]="count"></counter-display>
    <div>
      <counter-button (buttonClicked)="increment()">+ Increment</counter-button>
      <counter-button (buttonClicked)="decrement()">- Decrement</counter-button>
    </div>
  `,
    // Set change dection strategy to "OnPush"
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent implements OnInit, OnDestroy {
//  ...
}
Enter fullscreen mode Exit fullscreen mode

When change detection is set to ChangeDetectionStrategy.OnPush Angular only triggers change detection mechanisms when the inputs of the component are updated. Let's try manually call change detection and see if it solves the problem.

@Component({
  selector: 'counter',
  template: `
    <counter-display [value]="count"></counter-display>
    <div>
      <counter-button (buttonClicked)="increment()">+ Increment</counter-button>
      <counter-button (buttonClicked)="decrement()">- Decrement</counter-button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent implements OnInit, OnDestroy {
  count = 0;
  interval;

  // Inject change detector
  constructor(private cdr: ChangeDetectorRef) {}

  ngOnInit() {
    this.interval = setInterval(() => {
      this.increment();
      this.cdr.detectChanges();
    }, 2000);
  }

  ngOnDestroy() {
    if (this.interval) {
      clearInterval(this.interval);
    }
  }

  increment() {
    this.count++;
  }
  decrement() {
    this.count--;
  }
}
Enter fullscreen mode Exit fullscreen mode

Although our view is properly updated now, using change detector is not a prefered way to go. Fortunately, Angular is shipped with RxJS, a library, which allows writing reactive code using Observable pattern. This library allows operating streams of values. We can subscribe to those streams in the component which will ensure proper view updates when the stream returns a new value to the subscriber.

@Component({
  selector: 'counter',
  template: `
    <counter-display [value]="count$ | async"></counter-display>
    <div>
      <counter-button (buttonClicked)="increment()">+ Increment</counter-button>
      <counter-button (buttonClicked)="decrement()">- Decrement</counter-button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent implements OnInit, OnDestroy {
  // BehavioralSubject allows initiate steams with an initial value
  count$ = new BehaviorSubject(0);
  interval;

  ngOnInit() {
    this.interval = setInterval(() => {
      this.increment();
    }, 2000);
  }

  ngOnDestroy() {
    if (this.interval) {
      clearInterval(this.interval);
    }
  }

  increment() {
    const currentValue = this.count$.getValue();
    this.count$.next(currentValue + 1);
  }
  decrement() {
    const currentValue = this.count$.getValue();
    this.count$.next(currentValue - 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Whereas you can explicitly subscribe to the stream value in the component, in Angular it's common to use async pipe that takes care of subscribing and unsubscribing from the stream.

Summary

One of the greatest superpowers of any frontend framework is the ease of managing component state. State awareness allows not only building interaction-rich applications but also meeting complex business requirements.
In this episode, we've investigated React hooks - an easy way to manage state in the React functional components, as well as the Angular way of dealing with the component state via class variables. We have also touched a topic change detection in Angular and considered some special cases of performing state updates asynchronously.

Further reading

React:

  1. State Hook
  2. Effect Hook
  3. State and Lifecycle

Angular:

  1. Component Interactions
  2. The RxJS library
  3. ChangeDetectorRef

Top comments (2)

Collapse
 
ishansrivastava profile image
Ishan Srivastava

Hell yeah Angular ChangeDetection deserves an episode of its own 💪💪. Awesome article 👍

Collapse
 
glebirovich profile image
Gleb Irovich

Thanks for your feedback 🥳 will keep it rolling!