Do we need state management in Angular?

Balázs Kovács
4 min readOct 18, 2023

--

Why of course! It is usually a no-brainer to add some kind of state management library to the stack right at the start…

…at least that’s what I assumed whenever I started an Angular project in the past. While maintaining those projects though, often times it turned out a major setback to work with state management, while seemingly little to none benefits gained. Let’s take a closer look at the pros, cons & alternatives of state management in Angular!

Benefits

According to Salem Naser state management supports our application by

1. Sharing data between components: Components can access the shared state and display the data to the user or modify it based on user interactions.

2. Handling complex application state: As applications grow in complexity, managing state can become challenging. State management techniques provide patterns and tools to handle complex state scenarios effectively.

3. Enabling predictable data flow: State management provides a clear data flow pattern, ensuring that state changes are handled consistently and in a controlled manner.

4. Separating concerns: By centralizing the state, it separates the responsibility of managing data from the components, allowing for cleaner and more modular code.

Furthermore, using a popular library (like NgRx or NGXS) holds the added benefit of

  • flattening the learning curve (newcomers usually already have some experience with one of those libraries)
  • a rich ecosystem of extensions & community support

Drawbacks

Often using a state manager is just way too verbose comparing to what it achieves. Take a look at the example below (written using NGXS):

export class LoginComponent implements OnInit {
@Select(ConfigState.getTokenExpirationTime)
public tokenExpirationTime$!: Observable<string>;

constructor(private _store: Store) {}

ngOnInit(): void {
this._store.dispatch(new SetTokenExpirationTime());
}
}

export class SetTokenExpirationTime {
public static readonly type = '[Config] SetTokenExpirationTime';
}

export class ConfigStateModel {
public tokenExpirationTime?: number;
}

const defaults: ConfigStateModel = {
tokenExpirationTime: 1800,
};

@State<ConfigStateModel>({
name: 'config',
defaults,
})
@Injectable()
export class ConfigState {
constructor(private apiService: ApiService) {}

@Selector()
public static getTokenExpirationTime(state: ConfigStateModel): number | undefined {
return state.tokenExpirationTime;
}

@Action(SetTokenExpirationTime)
public setTokenExpirationTime(context: StateContext<ConfigStateModel>) {
this.apiService.get('/token-expiration-time').subscribe({
next: (response) => {
context.patchState({ tokenExpirationTime: response.data });
},
});
}
}

Most of the times this complexity is just unnecessary and has a negative impact on code quality and maintainability. Furthermore, it encourages thoughtlessly throwing all data into a global store instead of defining state scopes. Component state (e.g. persisting form values) should stay at component level — there’s no need to handle that globally.

Alternatives

There are many libraries focusing on keeping a minimal API. If you must use some kind of state manager I recommend to check out @stencil/store or StateAdapt, they’re both great at what they’re doing. NgRx itself also provides an alternative solution to its global store named @ngrx/component-store that helps to manage local/component state.

But… what if we do not need state management at all?

To store application state Angular uses services (centralized) and components (decentralized) by default. Combined with RxJS’s BehaviorSubject we have all the tools to achieve the same functionality as above, without using any state management library:

export class LoginComponent implements OnInit {
public tokenExpirationTime$ = this._configService.tokenExpirationTime$;

constructor(private _configService: ConfigService) {}

ngOnInit(): void {
this._configService.setTokenExpirationTime();
}
}

@Injectable()
export class ConfigService {
public readonly tokenExpirationTime$ = new BehaviorSubject(1800);

constructor(private apiService: ApiService) {}

public setTokenExpirationTime() {
this.apiService.get('/token-expiration-time')
.subscribe(this.tokenExpirationTime$);
}
}

Using no additional libraries has the benefits of reduced complexity & smaller build footprint, also can save a lot of maintenance on the long run. Going “vanilla” you might have to be more vigilant at code reviews, but a strong review culture can go miles even with the simplest tooling.

One more thing

I am very much still in progress of proving the theory in practice. As a side effect I’ve implemented a minimal package named @simple-persist/core to solve the one thing I missed from the “vanilla” approach: persistence of state. Take a look at this example:

@Injectable()
export class ConfigService {
@PersistSubject()
public readonly tokenExpirationTime$ = new BehaviorSubject(1800);
(...)
}

If interested, check out the package, any feedback is welcome!

--

--

Balázs Kovács

Software engineer, IT educator and team leader at Stylers Group