19

Angular State Management without external libraries

 5 years ago
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.
neoserver,ios ssh client

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$ as BehaviorSubject and assign with an initial value of undefined just to visualize that no value has been stored yet.
  • Next is the getAll function which returns the current value of the store and getAll$ that returns an Observable 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>
 

QnMFZ3F.png!web

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK