Over the years best practices for state management have solidified in Angular, and there are just so many choices.
NGRX Store and friends are a popular choice, but based on npm
downloads there are less than 20%
of Angular projects using it.
Table of contents
That means a huge percentage are using other libraries, or simply rolling their own Observables.
If that’s you, or perhaps you’ve been confused by @ngrx/store
and want to start with something a bit simpler - this post is for you.
That’s not to say Observables are “simple”, but by understanding how to create your own “reactive store” with Angular, it’ll help make a transition into expanding the idea into your own applications or using a library like NGRX and friends.
Concepts and Usage
Before we dive in, we need to think about the design of a reactive store. I see many developers simply creating Observables here and there, copy/pasting from random StackOverflow posts and piecing something together they don’t quite understand.
How do we want to consume the data once it’s in the store? NGRX provides a select()
method that we can use, so let’s go for that:
@Component({...})
export class AppComponent implements OnInit {
tasks$: Observable<Task[]>;
constructor(private store: TaskStore) {}
ngOnInit() {
this.tasks$ = this.store.select((state) => state.tasks);
}
}
That’s nice and simple, right? We can pass in a function which returns the state we want from our reactive store.
Let’s set it up.
Model Interfaces and Initial State
First, we’re going to setup an interface
or two and create some initialState
. This allows us to pass any initial state information into the store and pass it to any components:
interface Task { id: string; title: string, members: string[]; }
interface TaskState { tasks: Task[]; }
const initialState: TaskState = {
tasks: []
};
Then, we need an @Injectable
Service and drop in a BehaviorSubject
…
Injectable and BehaviorSubject
I like to attach this to a private _state
property:
@Injectable({ providedIn: 'root' })
export class TaskStore {
private _state: BehaviorSubject<TaskState>;
constructor() {
this._state = new BehaviorSubject<TaskState>(initialState);
}
}
BehaviorSubject
differs to aSubject
in two ways. First, we can pass an initial state value to aBehaviorSubject
, whereas withSubject
we cannot. Second,BehaviorSubject
passes the last value to new subscribers, whereasSubject
does not and only notifies on new values.
At this point, the basis for a store is almost there, but where do we introduce an Observable
? This is where many developers trip up. Is the BehaviorSubject
an Observable
? How do I get an Observable
?
Strictly speaking, a BehaviorSubject
is both an “observer” and type of observable.
If we want to fetch new values from our BehaviorSubject
we need to ask for it as an Observable
.
For this, I like to use a state$
property with the $
suffix, a naming convention to denote an Observable.
I use an accessor (setters and getters) to allow us to simply reference store.state$
to get our state as an Observable:
@Injectable({ providedIn: 'root' })
export class TaskStore {
private _state: BehaviorSubject<TaskState>;
constructor() {
this._state = new BehaviorSubject<TaskState>(initialState);
}
get state$(): Observable<TaskState> {
return this._state.asObservable();
}
}
Our component can then consume the store data right away:
@Component({...})
export class AppComponent implements OnInit {
tasks$: Observable<Task[]>;
constructor(private store: TaskStore) {}
ngOnInit() {
this.tasks$ = this.store.state$.pipe(
map(state => state.tasks)
);
}
}
Obviously this.tasks$
would be passed into an Async Pipe to create the subscription in the template.
You’ll note at this point we have to introduce .pipe(map(...))
after our store.state$
to fetch the property we want.
How about a custom select()
method?
Create a select() method
This seems a good place to loop back to create a select()
method that accepts a function:
@Injectable({ providedIn: 'root' })
export class TaskStore {
private _state: BehaviorSubject<TaskState>;
constructor() {
this._state = new BehaviorSubject<TaskState>(initialState);
}
get state$(): Observable<TaskState> {
return this._state.asObservable();
}
select<K>(selector: (state: TaskState) => K): Observable<K> {
return this.state$.pipe(
map(selector),
distinctUntilChanged()
);
}
}
Here we’re using a generic type of K
which is the returned state from our selector
function. This provides some type-safety features for our method.
Check out what we’ve done so far in the StackBlitz embed:
Create a setState() method
Now time to start setting state in the store.
I prefer to ‘hide’ the implementation details when setting state, which means we can keep things nice and clean for us.
It also means it’s time to start passing new values into our BehaviorSubject
.
There’s a few moving parts in this next piece, but have a look and I’ll explain after:
@Injectable({ providedIn: 'root' })
export class TaskStore {
private _state: BehaviorSubject<TaskState>;
constructor() {
this._state = new BehaviorSubject<TaskState>(initialState);
}
get state$(): Observable<TaskState> {
return this._state.asObservable();
}
get state(): TaskState {
return this._state.getValue();
}
setState<K extends keyof TaskState, E extends Partial<Pick<TaskState, K>>>(
fn: (state: TaskState) => E
): void {
const state = fn(this.state);
this._state.next({ ...this.state, ...state });
}
select<K>(selector: (state: TaskState) => K): Observable<K> {
return this.state$.pipe(map(selector), distinctUntilChanged());
}
}
First, I’ve added get state()
which returns us a state snapshot of the current store value as a plain JavaScript object.
Secondly, our setState()
method takes a callback fn
which the current state is then passed into - allowing us to call it like this:
this.store.setState((state) => {
// do something with `state` snapshot
return {
// return composed new state here
};
});
What’s important to note is that this._state.next()
is where we pass our new composed state back into the BehaviorSubject
, which will notify all subscribers.
Try it by clicking “Add Task”:
At this point we have a fully functional reactive store with a BehaviorSubject
, however the store is not very generic and is very closely coupled with our TaskState
.
Could we make it more generic?
Abstract Store Class
Let’s introduce a store-level generic type and make our class abstract
.
An abstract
class means it is never called directly, i.e. new Class()
, it is only extended for inheritance purposes.
We can refactor each TaskState
to T
to act as a generic type and rename a few things, namely Store<T>
.
Also note that we want to supply some initialState
into the constructor
, for this I’ve used the @Inject()
decorator which allows us to pass our own data in - instead of an Angular dependency for injection:
@Injectable({ providedIn: 'root' })
export abstract class Store<T> {
private _state: BehaviorSubject<T>;
constructor(@Inject('') initialState: T) {
this._state = new BehaviorSubject<T>(initialState);
}
get state$(): Observable<T> {
return this._state.asObservable();
}
get state(): T {
return this._state.getValue();
}
setState<K extends keyof T, E extends Partial<Pick<T, K>>>(
fn: (state: T) => E
): void {
const state = fn(this.state);
this._state.next({ ...this.state, ...state });
}
select<K>(selector: (state: T) => K): Observable<K> {
return this.state$.pipe(map(selector), distinctUntilChanged());
}
}
Now our Store<T>
is generic, it means we can create multiple reactive services that simply inherit all this powerful functionality - so we can stop worrying so much about managing BehaviourSubject
and all the Observable
goodness. It’s hidden!
This is how we’d then use it with a TaskService
:
interface TaskState { tasks: Task[]; }
const initialState: TaskState = {
tasks: []
};
@Injectable({ providedIn: 'root' })
export class TaskService extends Store<TaskState> {
constructor() {
super(initialState); // pass initial state
}
addTask(task: Task) {
this.setState((state) => ({
tasks: [...state.tasks, task]
}));
}
}
Our addTask
method now can live in the TaskService
, holding our business logic for managing the state
. You can think of this as an “Action” method, which returns new state much like a “Reducer” would in the Redux paradigm.
Take a look now at our component class, it’s super lean and only communicates outward via the TaskService
, creating a nice separation of concern whilst offloading the work to a service.
The benefit of this too is that it can be used in many components and use the same methods:
Each Service
we then create can act as an independent Store
, inherit our functionality and the sky’s the limit.
@ultimate/lite-store
I actually took this idea further and open sourced a new state management library using this code and a little extra to provide Entity Pattern support, automatic frozen state via Object.freeze()
and a few other features like createSelector()
.
You can check the @ultimate/lite-store’s full source code here and note that most of the code we’ve explored today is readily available for you to use as a neat little package.
If you’re interested in using it, check out the @ultimate/lite-store documentation and get it installed in your project via npm install @ultimate/lite-store
. It’s lean, minimal and focused on just being simple. It’s fully typed, tested and ready to go.
I hope you enjoyed creating the Store
in this article. We’ve learned a great deal, especially if you’re new to Angular, RxJS an all that comes with it.
🚀 To dive in headfirst and learn how to architect real Angular apps with me, I’d highly recommend checking out my brand new Angular courses.
Happy coding!