DEV Community

loading...
Cover image for Simple state management in Angular with only Services and RxJS

Angular State Management Simple state management in Angular with only Services and RxJS

avatsaev profile image Aslan Vatsaev Updated on ・5 min read

One of the most challenging things in software development is state management. Currently there are several state management libraries for Angular apps: NGRX, NGXS, Akita... All of them have different styles of managing state, the most popular being NGRX, which pretty much follows the FLUX/Redux principles from React world (basically using one way data flow and immutable data structures but with RxJS observable streams).

But what if you don't want to learn, setup, use an entire state management library, and deal with all the boilerplate for a simple project, what if you want to manage state by only using tools you already know well as an Angular developer, and still get the performance optimisations and coherency that state management libraries provide (On Push Change Detection, one way immutable data flow).

DISCLAIMER: This is not a post against state management libraries. We do use NGRX at work, and it really helps us to manage very complex states in very big and complex applications, but as I always say, NGRX complicates things for simple applications, and simplifies things for complex applications, keep that in mind.

In this write up, I'll show you a simple way of managing state by only using RxJS and Dependency Injection, all of our component tree will use OnPush change detection strategy.

Imagine we have simple Todo app, and we want to manage its state, we already have our components setup and now we need a service to manage the state, let's create a simple Angular Service:

// todos-store.service.ts

@Injectable({provideIn: 'root'})
export class TodosStoreService {


}

Enter fullscreen mode Exit fullscreen mode

So what we need is, a way to provide a list of todos, a way to add todos, remove, filter, and complete them, we'll use getters/setters and RxJS's Behaviour Subject to do so:

First we create ways to read and write in todos:

// todos-store.service.ts

@Injectable({provideIn: 'root'})
export class TodosStoreService {

  // - We set the initial state in BehaviorSubject's constructor
  // - Nobody outside the Store should have access to the BehaviorSubject 
  //   because it has the write rights
  // - Writing to state should be handled by specialized Store methods (ex: addTodo, removeTodo, etc)
  // - Create one BehaviorSubject per store entity, for example if you have TodoGroups
  //   create a new BehaviorSubject for it, as well as the observable$, and getters/setters
  private readonly _todos = new BehaviorSubject<Todo[]>([]);

  // Expose the observable$ part of the _todos subject (read only stream)
  readonly todos$ = this._todos.asObservable();


  // the getter will return the last value emitted in _todos subject
  get todos(): Todo[] {
    return this._todos.getValue();
  }


  // assigning a value to this.todos will push it onto the observable 
  // and down to all of its subsribers (ex: this.todos = [])
  private set todos(val: Todo[]) {
    this._todos.next(val);
  }

  addTodo(title: string) {
    // we assaign a new copy of todos by adding a new todo to it 
    // with automatically assigned ID ( don't do this at home, use uuid() )
    this.todos = [
      ...this.todos, 
      {id: this.todos.length + 1, title, isCompleted: false}
    ];
  }

  removeTodo(id: number) {
    this.todos = this.todos.filter(todo => todo.id !== id);
  }


}

Enter fullscreen mode Exit fullscreen mode

Now let's create a method that will allow us to set todo's completion status:

// todos-store.service.ts


setCompleted(id: number, isCompleted: boolean) {
  let todo = this.todos.find(todo => todo.id === id);

  if(todo) {
    // we need to make a new copy of todos array, and the todo as well
    // remember, our state must always remain immutable
    // otherwise, on push change detection won't work, and won't update its view    

    const index = this.todos.indexOf(todo);
    this.todos[index] = {
      ...todo,
      isCompleted
    }
    this.todos = [...this.todos];
  }
}

Enter fullscreen mode Exit fullscreen mode

And finally an observable source that will provide us with only completed todos:

// todos-store.service.ts

// we'll compose the todos$ observable with map operator to create a stream of only completed todos
readonly completedTodos$ = this.todos$.pipe(
  map(todos => todos.filter(todo => todo.isCompleted))
)

Enter fullscreen mode Exit fullscreen mode

Now, our todos store looks something like this:

// todos-store.service.ts


@Injectable({providedIn: 'root'})
export class TodosStoreService {

  // - We set the initial state in BehaviorSubject's constructor
  // - Nobody outside the Store should have access to the BehaviorSubject 
  //   because it has the write rights
  // - Writing to state should be handled by specialized Store methods (ex: addTodo, removeTodo, etc)
  // - Create one BehaviorSubject per store entity, for example if you have TodoGroups
  //   create a new BehaviorSubject for it, as well as the observable$, and getters/setters
  private readonly _todos = new BehaviorSubject<Todo[]>([]);

  // Expose the observable$ part of the _todos subject (read only stream)
  readonly todos$ = this._todos.asObservable();


  // we'll compose the todos$ observable with map operator to create a stream of only completed todos
  readonly completedTodos$ = this.todos$.pipe(
    map(todos => todos.filter(todo => todo.isCompleted))
  )

  // the getter will return the last value emitted in _todos subject
  get todos(): Todo[] {
    return this._todos.getValue();
  }


  // assigning a value to this.todos will push it onto the observable 
  // and down to all of its subsribers (ex: this.todos = [])
  private set todos(val: Todo[]) {
    this._todos.next(val);
  }

  addTodo(title: string) {
    // we assaign a new copy of todos by adding a new todo to it 
    // with automatically assigned ID ( don't do this at home, use uuid() )
    this.todos = [
      ...this.todos, 
      {id: this.todos.length + 1, title, isCompleted: false}
    ];
  }

  removeTodo(id: number) {
    this.todos = this.todos.filter(todo => todo.id !== id);
  }

  setCompleted(id: number, isCompleted: boolean) {
    let todo = this.todos.find(todo => todo.id === id);

    if(todo) {
      // we need to make a new copy of todos array, and the todo as well
      // remember, our state must always remain immutable
      // otherwise, on push change detection won't work, and won't update its view
      const index = this.todos.indexOf(todo);
      this.todos[index] = {
        ...todo,
        isCompleted
      }
      this.todos = [...this.todos];
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

Now our smart components can access the store and manipulate it easily:

(PS: Instead of managing immutability by hand, I'd recommend using something ImmutableJS)

// app.component.ts

export class AppComponent  {
  constructor(public todosStore: TodosStoreService) {}
}

Enter fullscreen mode Exit fullscreen mode
<!-- app.component.html -->

<div class="all-todos">

  <p>All todos</p>

  <app-todo 
    *ngFor="let todo of todosStore.todos$ | async"
    [todo]="todo"
    (complete)="todosStore.setCompleted(todo.id, $event)"
    (remove)="todosStore.removeTodo($event)"
  ></app-todo>
</div>

Enter fullscreen mode Exit fullscreen mode

And here is the complete and final result:

Full example on StackBlitz with a real REST API

This is a scalable way of managing state too, you can easily inject other store services into each other by using Angular's powerful DI system, combine their observables with pipe operator to create more complex observables, and inject services like HttpClient to pull data from your server for example. No need for all the NGRX boilerplate or installing other State Management libraries. Keep it simple and light when you can.


Follow me on Twitter for more interesting Angular related stuff: https://twitter.com/avatsaev

Discussion (68)

Collapse
asparallel profile image
AsParallel

Definitely my preference. Generally the applications I find myself building don't need anything more than this, and pure observable contexts make things so much more composable, no magic strings anywhere.

Collapse
ova2 profile image
Oleg Varaksin

Exactly what I'm trying to do in my app. An advice: don't put HTTP, async services, etc. into the store. Keep it separate. State management has nothing to do with such services and business logic. Ngrx Effects is a terrible mix of two concepts.

Collapse
avatsaev profile image
Aslan Vatsaev Author

You're right, side effects must always be separated from state management, this was a quick example, i'll try to clean it up when I have some free time.

Collapse
lysofdev profile image
Esteban Hernández

I've almost made a career out of reducing the complexity and size of Angular 2+ apps by appropriately using services and RxJs instead of convoluted, home-grown solutions to state management.

My resume:

  • Read the Angular docs past the 2nd page.
Collapse
ronancodes profile image
Ronan Connolly 🛠

Very well put together, succinct article!
We don't use NgRX at my workplace, but we do have a couple of data source services.
They kind of just grew organically.

I see the benefits of following your rules of immutability, having a private behaviourSubject, and the getter and setter. Also the second readonly observable which pipes the behaviourSubject is very nice.

Is the term storeService a widely used suffix?
I currently use dataSourceService.

Collapse
avatsaev profile image
Aslan Vatsaev Author • Edited

Thanks!

Is the term storeService a widely used suffix?

nope it's up to you to name it, I usually name it something like TodosStore

Collapse
ronancodes profile image
Ronan Connolly 🛠 • Edited

I think I prefer the suffix store over dataSource.
Also the ngrx stuff talks about stores a lot. Seems to be a popular term.

Collapse
chrismarx profile image
chrismarx • Edited

As a few others have pointed out, this is basically Akita. Akita gives you all the basic crud methods you need to start managing either an array of entities (todos) or a single entity, but with none of the boilerplate required by ngrx or ngxs. There's almost no setup, and in addition to getting a rxjs-based approach to state management, you also get all the other bells and whistles, like action tracking with the redux dev tools, time travelling, action tracing, etc. I started out with rxjs stores, and then tried ngrx, ngxs, went back to rxjs stores, then finally found akita, and I see no reason to ever not use it-

github.com/datorama/akita

Collapse
evanboissonnot profile image
Evan BOISSONNOT

Hi, some perf prob from Akita ? medium.com/@vpranskunas/deep-compa...

I figure out if you get these probs too ?

Collapse
iain_adams profile image
Iain Adams

This is exactly what I do in my project. I trialled ngrx/store but found it so overly bloated with a ridiculous amount of boilerplate for very little gain. This pattern is simple, elegant, easy to reason about.

Collapse
lysofdev profile image
Esteban Hernández

Give ngrx/data a look for a more streamlined version of ngrx/store. It's excellent for managing collections of objects. It provides almost all of the functionality you might need out of the box and is based on configuration so you end up writing very little logic.

Collapse
drewstaylor profile image
Drew Taylor

Honestly, unless your app is extremely complicated, anyone using a state management library for Angular has misunderstood the component life cycle and is pretty much trying to make Angular into a React application.

Collapse
cwspear profile image
Cameron Spear

This looks really nice. After 4ish projects across React and Angular, I have yet to find the Redux pattern remotely worth it... But I'll say that has a lot to do with particular executions...

Regardless, this seems like this would make life a lot easier. :)

One question, tho: is the shareReplay in completedTodos$ not redundant? It would call shareReplay twice in a row in the pipe.

Collapse
avatsaev profile image
Aslan Vatsaev Author

To be honest I'm not sure, I've put it for good measure, but theoretically yes, i didn't have to use shareReplay on filtering considering that the original source is already multicast, I'll do some tests to confirm, and remove it later.

Collapse
spierala profile image
Florian Spier

Hey Aslan! I wrote a similar article on DEV. It is inspired by yours! thanks!
dev.to/angular/simple-yet-powerful...

TLDR Let’s create our own state management Class with just RxJS/BehaviorSubject (inspired by some well known state management libs).

Collapse
fbrun profile image
Fred • Edited

Thanks for this post, it's help us very well.

When you define the "readonly completedTodos$" in the service, you use the getter (this.todos) in the map operator to filter the stream. What doesn't use directly the "todos" var entry from the map operator like this:
readonly completedTodos$ = this.todos$.pipe(
map(todos => todos.filter(todo => todo.isCompleted))
) ?

Collapse
avatsaev profile image
Aslan Vatsaev Author

Hey, both do the same thing, but yes you are correct, using todos from the map makes more sense, i updated the article.

Collapse
learnitmyway profile image
David • Edited

Thanks for the article! Some points/questions for someone who is quite new to Angular:

  1. What made you choose to use a view child?
  2. Thank you for including todosTrackFn. Didn't know that existed!
  3. Can the getter and setter be private? Or are they used outside of the store?
  4. Nice work with the optimistic updates!
  5. Since you are defining the todo id in the client, does it need to be optional in the model? Or is it not set somewhere?
Collapse
pavelnm profile image
pavelNM • Edited

why use behavior subject with sharereplay?
get todos() is also overkill. It's not a good way to get data from observer, except in async pipe. For adding todo we should use scan operator, same as for mark as completed. Subject with shareReplay (or ReplaySubject) solves all the problems and BehaviorSubject is really redundant here.

Collapse
avatsaev profile image
Aslan Vatsaev Author

Thanks for the feedback, updated.

Collapse
chuckirvine profile image
Chuck

I notice that the StackBlitz project refers to an undefined trackBy function:

todosTrackFn

I can't see any problem that it is causing. The project compiles ok. TSLint flags the problem in my vscode edit buffer.

Collapse
avatsaev profile image
Aslan Vatsaev Author

yep it's a typo, fixed it on stackblitz

Collapse
jasonpolitis profile image
Jason Politis

Accessors for the same member name must specify the same accessibility

todos getter/setters must either both be public or both be private. My preference is they are private, so that publicly, only the observable is accessible.

Another alternative is make private set _todos and public get todos.

You probably already know better than me which options are best, but thought I throw some options out there for others that come across this.

Thanks for the excellent example. Do you by any chance have an example for todosGroup and todos, and how they would work with each other when navigating from group to single todo?

Thank you for sharing. I value your input as a developer and person. :D

Collapse
chuckirvine profile image
Chuck

Thanks for this. Have been exploring NgRx and have been feeling "this isn't worth it". Did a google search for "simple Angular state management" and here I am.

Collapse
avatsaev profile image
Aslan Vatsaev Author • Edited

"this isn't worth it"

depends on the case, when I work on large scale apps with a lot of moving parts, it's very worth it

Collapse
nigrosimone profile image
Nigro Simone

Inspired by this article, i have made a library "ng-simple-state" based on simple state management in Angular with only Services and RxJS npmjs.com/package/ng-simple-state

I have also implemented the support to Redux DevTools browser extension and localstorage state persistence.

Thanks for your inspirational post.

Collapse
sheldonhage profile image
Sheldon Hage • Edited

Hello Aslan,

I found a critical problem with this solution (which btw I found while trying to solve the same critical problem with my very own nearly identical code).

After literally weeks of troubleshooting this I am backed up against the wall and considering alternatives to Angular because I need the features that (it appears) only BehaviorSubject can solve.

But for now let's look at your Stackblitz example. At first glance it works perfectly. Bravo. I said to myself the same thing on my own code.

Then, try these things and you will soon realize there is a very serious problem and it is difficult to solve: corruption in the two arrays.

Recreate the problems in two methods:
Method 1:
Open your stackblitz and click rapidly on items in the incomplete and completed lists. Mark items as complete and the others as incomplete.... keep doing it maybe 20 times. You will see the following corruption take place:
a) Items will appear more than once on each list
b) Clicking an item to complete it will result in another already-completed item becoming incomplete and vice versa.
c) Clicks being ignored by the UI

Method 2:
Leave your stackblitz open in chrome and untouched for an hour or so. Come back and notice the first click (lets say mark an item as complete) is dead (or it appears dead). But upon the next click you will be able to add the same item twice to the completed list.

I believe there is an async / await sort of sequence issue that because of template code executing faster than service code, the UI will show array values that differ from what is in state service behaviorsubject.

Your example behaves just like my own.

I am not certain why more people have not noticed? Any ideas as to the root cause? I would be happy to demonstrate this over a zoom call.

Collapse
nhh profile image
Niklas

Absolutely brilliant! Always know your actual toolset before buying into new ones!

Collapse
nickamorim profile image
Nick • Edited

First of all, thanks for the article.
Its really great, and certainly expanded my horizons on the use of RxJS.

Just one question, on stack blitz you use toPromise() on the calls to the rest api. This could not also be achieved using observables?

this.todosService.setCompleted(id, isCompleted)
.pipe(take(1))
.subscribe({
next: value => {},
error: err => {
console.error(e);
this.todos[index] = {
...todo,
isCompleted: !isCompleted
}
}
});

I did see that you use it because of async/wait but not understand why.
I'm just trying to understand the difference between the two approaches.

Thanks!

Collapse
snesi profile image
David Domingo

Hey Aslan, I do the same thing, except I don't understand the need for sharedReplay. You're already sharing the same observable because it's assigned to a public readonly property. And you're also using a BehaviourSubject which should always return the current value to new subscribers. Also, why not just use a ReplaySubject if you need the previous value? You lose getValue, but it's not such a big deal because you probably want to subscribe anyway. What am I missing?

Collapse
avatsaev profile image
Aslan Vatsaev Author • Edited

BehaviourSubject is not a simple observable, it's an observer AND an observable at the same time that can give you its latest state synchronously.

You're already sharing the same observable because it's assigned to a public readonly property

That doesn't mean the observer is shared, and that doesn't change the fact that when someone subscribes to todos$, a new dedicated observer will be created for it. A shared observable means that all subscribers are getting their data stream from a single observable source. (lookup rxjs multicasting)

More info here: learnrxjs.io/operators/multicastin...

Also, why not just use a ReplaySubject if you need the previous value? You lose getValue...

I need to be able to get the previous state synchronously in the reducers, as I already explained in my other comment, reducers are synchronous functions that take previous state and reduce it to a new state in an immutable way.

The reason we need the replay part is because some components might be rendered asynchronously (after fetching some data from api for ex), and might miss the emits in todos$ observable, so when they subscribe to it they won't know what happened in the source before component came to life, the replay will emit the latest value to all observers upon subscription.

On the other hand, the second share replay is not necessary, I updated the code.

Hope I answered your questions.

Collapse
petivagyok16 profile image
Peter Palocz

Whats your opinion about using this method in a complex application that doesn't use any state management at all yet (due to lack of initial planning), but our team would like to change this in the future regarding modules one by one starting with the most complex one. Is it a good idea to go with this one, or would it be better idea to go with ngrx because of the complexity?

Collapse
avatsaev profile image
Aslan Vatsaev Author

Depends on the scale, if you have a big app with a lot of components than need to share state, and a lot of moving parts, i strongly suggest ngrx, i used this method (the one described in the article) to build several libraries that have internal state, and it works very well, so no need to use ngrx in component libs.

Collapse
nigrosimone profile image
Nigro Simone

Inspired by this article, i have made a library "ng-simple-state" based on simple state management in Angular with only Services and RxJS npmjs.com/package/ng-simple-state

I have also implemented the support to Redux DevTools browser extension and localstorage state persistence.

Thanks for your inspirational post.

Collapse
sebbdk profile image
Sebastian Vargr

If you are reaching this level of complexity with more than 1-2 services then i would suggest start looking into using a library with some more restraints.

At least if you are more than 1-2 devs touching the codebase.

Otherwise long-term feature additions and bugfixes are likely to turn this into state soup eventually, once developers start to slack a bit or implement newer idea's.

Which is bound to happen sooner or later in a system without opinionated restraints.

I liked the demo service example, but would have liked to see some with less observable bootstrapping, and maybe just some general guidelines.

Since that's what i find smaller projects benefit more from in my experience. :)

Found the article a good read however, thanks!

Collapse
ellisium profile image
Rich Ellis

This was my thought as well.

From looking around it seems like (as of today) Akita basically is this approach, plus some organizational patterns/practices that should help encourage consistency.

I do wish there was a library out there with a longer track record that was lighter than NGRX.

Collapse
gabrielaraujof profile image
Gabriel Araujo

I really liked this approach and I'm definitively going to use it! But I wonder how could it be used when we actually have to fetch data from an API?

Collapse
avatsaev profile image
Aslan Vatsaev Author

It's easy, i'll make an example soon

Collapse
klouddy profile image
Jake

Aslan, I like this approach quite a bit, but I have the same questions. What would it look like when fetching data from an API?

Thread Thread
avatsaev profile image
Aslan Vatsaev Author • Edited

Hey @klouddy @gabrielaraujof , I've updated the stackblitz example with a real REST API and some interesting techniques on how to do optimistic updates and rollbacks: stackblitz.com/edit/angular-rxjs-s...

Collapse
avatsaev profile image
Aslan Vatsaev Author

I've updated the stackblitz example with a rest api

Collapse
k0d3d profile image
Michael Rhema

If this works out for me, and I implement it in 4 hours on my very crazy codebase... Im buying u coffee or a beer...

Collapse
avatsaev profile image
Aslan Vatsaev Author

good luck haha, 4 hours is quite a challenge, let me know how it went

Collapse
i41099195 profile image
I

Great article. Thank you.
I'm trying to adapt this kind of state managment to my app.
In my scenario I'm subscribing to some events (adding, removing) and do some other logic (for example showing message "you have removed something").
How would you modify your todo example to add this kind of logic in a separate file/ or in other components?

Collapse
1304654 profile image
1304654 • Edited

What difference is there in using BehaviorSubject in a state manager class instead of sharing a service class with properties passed by reference?

Collapse
avatsaev profile image
Aslan Vatsaev Author • Edited

onPush change detection, combining/manipulating data streams with RxJS, and it's really fast.

Collapse
1304654 profile image
1304654

Thanks!

Collapse
vladpavliuk profile image
Vlad • Edited

Hi, in the user's code I've noticed, that you pass 'event' to the remove method:
(remove)="todosStore.removeTodo($event)"

Shouldn't it be just an id?:
(remove)="todosStore.removeTodo(todo.id)"

Collapse
avatsaev profile image
Aslan Vatsaev Author

both are correct, if the app-todo-component outputs the todo id in the event, it'll still work

depends on the implementation of the @Ouptut

Collapse
vladpavliuk profile image
Vlad

Yea, I completely agree, they are the same, I guess there is a real small advantage to use todo.id instead of $event - if we use todo.id we explicitly say that we wanna to use id property from list of todos, otherwise if we use $event we delegate the responsibility to the child component to use 'remove' event properly (pass the correct value).

Collapse
suneric1 profile image
Zhiming Sun

A great example of clean and organized code.

Collapse
voodoorider profile image
Wojciech Owczarczyk

Shouldn't the getter and setter for todos be private as well?

Forem Open with the Forem app