Angular State Management without external libraries
source link: https://www.tuicool.com/articles/hit/7JZ3IjQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Angular State Management without using external libraries
Published by Rienz on January 24, 2019January 24, 2019
Angular State Management is important when it comes to web applications or web tools but some external libraries in Angular like ngrx
, ngxs
and akita
are very complicated and complex on how to implement it.
Now I would share something called BehaviorSubject
in rxjs
which is already included on Angular and can be used as a tool to manage our state. You can also go to this github repo to double check if your doing it correctly and know the structure of this article.
What is Subject?
We need to know first what is Subject
. Basically it is an Observable
in which you can store a value and emits the value upon using the next
function.
const subject = new Subject(); subject.next('1'); subject.subscribe(value => { console.log(value); }); // doesn't log 1 subject.next('2'); // logs 2 subject.next('3'); // logs 3
What is BehaviorSubject?
BehaviorSubject
is a type of the Subject
but requires an initial value and emits its latest or current value upon subscription. This is important for our Angular State Management since we could get the latest value of our data and update it whenever some changes happens.
const subject = new BehaviorSubject('0'); subject.next('1'); subject.subscribe(value => { console.log(value); }); // logs 1 since its the current value subject.next('2'); // logs 2 subject.next('3'); // logs 3
Creating Model and Service
Before we proceed on creating our Store class. I want to share the full potential of this topic so first lets create a Model which represents our state and Service that we can use later from our store which calls an HTTP request from an API.
export interface Post { id: number; userId: number; title: string; body: string; }
Checking on the routes that are available in jsonplaceholder . I picked the post endpoint for us to use in our project.
import { Observable } from 'rxjs'; import { Post } from './../models/posts.model'; import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class PostsService { private url: string; constructor( private http: HttpClient ) { this.url = 'https://jsonplaceholder.typicode.com/posts'; } get$ = (): Observable<Post[]> => this.http.get<Post[]>(this.url); post$ = (post: Post): Observable<Post> => this.http.post<Post>(this.url, { post }); patch$ = (postId: number, post: Post): Observable<Post> => this.http.patch<Post>(`${this.url}/${postId}`, { post }); delete$ = (postId: number): Observable<Post> => this.http.delete<Post>(`${this.url}/${postId}`); }
Nothing crazy here just some HTTP functions in our service that we will be using in our store later.
Creating Store
Now that we know how BehaviorSubject
works. Lets proceed on creating our Store
class.
import { BehaviorSubject, Observable } from 'rxjs'; export abstract class Store<T> { private state$: BehaviorSubject<T> = new BehaviorSubject(undefined); getAll = (): T => this.state$.getValue(); getAll$ = (): Observable<T> => this.state$.asObservable(); store = (nextState: T) => (this.state$.next(nextState)); }
Let me explain one by one on what did we do here.
- First we declare our
state$
asBehaviorSubject
and assign with an initial value ofundefined
just to visualize that no value has been stored yet. - Next is the
getAll
function which returns the current value of the store andgetAll$
that returns anObservable
on which we can subscribe so that our data will be updated when some changes happens in our state. - Lastly is the
store
function where we update the data on our state.
Extending Store
Here’s the fun part where we’re going to inject the Service and extend the Store. So now we’re going to extend our Store class to our PostsStore.
import { Injectable } from '@angular/core'; import { tap } from 'rxjs/operators'; import { PostsService } from '../services/posts.service'; import { Store } from './store'; import { Post } from '../models/posts.model'; @Injectable({ providedIn: 'root' }) export class PostsStore extends Store<Post[]> { constructor(private postsService: PostsService) { super(); } init = (): void => { if (this.getAll()) { return; } this.postsService.get$().pipe( tap(this.store) ).subscribe(); } }
Then provide an initialization init
function to get the data from the service and then use the store
function to save the response data.
Import in Module
Don’t forget to put the store and service inside the providers of the module. also add the HttpClientModule
into imports since we use HttpClient
in Service.
import { PostsStore } from './stores/posts.store'; import { PostsService } from './services/posts.service'; import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent, ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule ], providers: [PostsService, PostsStore], bootstrap: [AppComponent] }) export class AppModule { }
Display in Component
You can run the init
function wherever you like, for example in route resolver
, components
, and etc. But in this example I’m just going to run it on the App Component
just to show on how it works.
import { Post } from './models/posts.model'; import { Observable } from 'rxjs'; import { PostsStore } from './stores/posts.store'; import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { posts$: Observable<Post[]>; constructor( private postsStore: PostsStore ) { this.postsStore.init(); } ngOnInit() { this.posts$ = this.postsStore.getAll$(); } }
<ul *ngFor="let post of posts$ | async"> <li>ID: {{ post.id }}</li> <li>TITLE: {{ post.title }}</li> <li>BODY: {{ post.body }}</li> </ul>
We used an async pipe since our returned value is an observable but you can do it on the other way around where your going to subscribe to the observable then assign the returned value to your variable like the example below.
export class AppComponent implements OnInit { posts: Post[]; constructor( private postsStore: PostsStore ) { this.postsStore.init(); } ngOnInit() { this.postsStore.getAll$().subscribe(posts => { this.posts = posts; }); } }
<ul *ngFor="let post of posts"> <li>ID: {{ post.id }}</li> <li>TITLE: {{ post.title }}</li> <li>BODY: {{ post.body }}</li> </ul>
I would still prefer using async pipe as much as possible since it automatically unsubscribe the observable when the component is destroyed or not active but you can also manually unsubscribe the subscription however with extra codes.
Example Basic CRUD
import { Injectable } from '@angular/core'; import { tap } from 'rxjs/operators'; import { PostsService } from '../services/posts.service'; import { Store } from './store'; import { Post } from '../models/posts.model'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class PostsStore extends Store<Post[]> { constructor(private postsService: PostsService) { super(); } init = (): void => { if (this.getAll()) { return; } this.postsService.get$().pipe( tap(this.store) ).subscribe(); } create$ = (post: Post): Observable<Post> => this.postsService .post$(post) .pipe( tap(resPost => { this.store([ ...this.getAll(), { id: resPost.id, ...post } ]); }) ) update$ = (postId: number, post: Post) => this.postsService .patch$(postId, post) .pipe( tap(resPost => { const posts = this.getAll(); const postIndex = this.getAll().findIndex(item => item.id === postId); posts[postIndex] = { id: resPost.id, ...post }; this.store(posts); }) ) delete$ = (postId: number) => this.postsService .delete$(postId) .pipe( tap(() => { const posts = this.getAll(); const postIndex = this.getAll().findIndex(item => item.id === postId); posts.splice(postIndex, 1); this.store(posts); }) ) }
import { Post } from './models/posts.model'; import { Observable } from 'rxjs'; import { PostsStore } from './stores/posts.store'; import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { posts$: Observable<Post[]>; constructor( private postsStore: PostsStore ) { this.postsStore.init(); } ngOnInit() { this.posts$ = this.postsStore.getAll$(); } createPost() { this.postsStore.create$({ userId: 1, title: 'Codedam', body: 'Unlock Codes and It is Awsome' }).subscribe(post => { console.log(post); }); } updatePost() { this.postsStore.update$( 1, { userId: 1, title: 'Codedam', body: 'Unlock Codes and It is Awsome' }).subscribe(post => { console.log(post); }); } deletePost() { this.postsStore.delete$(1).subscribe(post => { console.log(post); }); } }
<ul *ngFor="let post of posts$ | async"> <li>ID: {{ post.id }}</li> <li>TITLE: {{ post.title }}</li> <li>BODY: {{ post.body }}</li> <li> <button (click)="createPost()">Create</button> <button (click)="updatePost()">Update</button> <button (click)="deletePost()">Delete</button> </li> </ul>
So yeah! This is the end about Angular State Management and how to make yourself a store without using any external libraries. If you have any questions or suggestions feel free to comment here and I will try to reply as soon as I can.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK