DEV Community

Cover image for State management with Angular 8 and Akita
Raj Vijay
Raj Vijay

Posted on • Updated on

State management with Angular 8 and Akita

Back in 2017 I wrote an article, building a simple blog application using NgRx here. Recently I have been experimenting with Akita, a state management pattern which I found to be much simpler and with less boilerplate code. So I decided to rewrite my sample app using Akita and would like to share the code with you.

Backend Server Setup

Let's use json-server to simulate our backend server. json-server helps us to setup a local development server for CRUD operations. Let's start with installing json-server.

npm install -g json-server

We will also create a JSON file with name db.json and add some sample entries for blogs and authors as show below.

{
  "blogs": [
    {
      "title": "Blog Title 1",
      "author": "John",
      "id": 1
    },
    {
      "title": "Blog Title 2",
      "author": "Harry",
      "id": 2
    }
  ],
  "authors": [
    {
   "id":1,
      "name": "All"
    },
    {
   "id":2,
      "name": "John"
    },
    {
   "id":3,
      "name": "Harry"
    },
    {
   "id":4,
      "name": "Jane"
    }
  ]
}

Let’s start the JSON server by running the command

json-server --watch db.json

This will setup a localhost server on your computer at port 3000. You should be able to navigate to http://localhost:3000/authors and see all the authors.

Blogs

First we need to return a list of blogs from the server. Let's add a new file blog.ts under models folder.

import { ID } from '@datorama/akita';

export interface Blog {
    id: ID;
    title: string;
    author: string;
}

Blog Store

Next, we create a blog store, this is where the blog state is going to be stored. In our sample application, we will have to hold an array of blogs and use a filter function to filter the blogs based on selected author. These are some of the states that we will be holding in our stores. Store can be viewed similar to a table in a database.

import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
import { Blog } from '../models/blog';
import { Injectable } from '@angular/core';

export interface BlogState extends EntityState<Blog> { }

@Injectable({
    providedIn: 'root'
})
@StoreConfig({ name: 'blogs' })
export class BlogStore extends EntityStore<BlogState, Blog> {

}

We will also need a filter store to save filter status, with initial value set to 'All'.

export interface FilterState extends EntityState<string> {
    authorFilter: {
        filter: string;
    };
}

const initialState = {
    authorFilter: {
        filter: 'All'
    }
};

@Injectable({
    providedIn: 'root'
})
@StoreConfig({ name: 'filter' })
export class FilterStore extends EntityStore<FilterState, string> {
    constructor() {
        super(initialState);
    }
}

Blog Query

We need a mechanism to query entities from the store. Akita docs recommends components should not get the data from the store directly but instead, use a Query. Let's create a query file and name it blog-query.ts.

import { QueryEntity } from '@datorama/akita';
import { Injectable } from '@angular/core';
import { BlogState, BlogStore } from '../stores/blog-store';
import { Blog } from '../models/blog';
@Injectable({
    providedIn: 'root'
})
export class BlogQuery extends QueryEntity<BlogState, Blog> {
    constructor(protected store: BlogStore) {
        super(store);
    }
}

Filter Query

Let's also create a filter query, add a file filter-query.ts. The getValue() method returns the raw value of the store, in our case the filter value.

export class FilterQuery extends QueryEntity<FilterState, string> {
    constructor(protected store: FilterStore) {
        super(store);
    }

    getFilter() {
        return this.getValue().authorFilter.filter;
    }

}

Blog Service

Akita recommends all asynchronous calls should be encapsulated in a service. So let's create a blog service and inject blog store into the service.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { Blog } from '../models/blog';
import { BlogStore } from '../stores/blog-store';

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

  filter = 'All';
  constructor(private http: HttpClient, private blogStore: BlogStore) {
  }

  private createBlog({ id, title, author }: Partial<Blog>) {
    return {
      id,
      title, author
    };
  }


  get() {
    return this.http.get<Blog[]>('http://localhost:3000/blogs').pipe(tap(blogs => {
      this.blogStore.set(blogs);
    }));
  }


  add({ title, author }: Partial<Blog>) {
    const blog = this.createBlog({ id: Math.random(), title, author });
    this.blogStore.add(blog);
  }

  delete(id) {
    this.blogStore.remove(id);
  }

}

Let's also create an author service to get a list of authors.

export class AuthorService {

  constructor(private authorStore: AuthorStore,
              private http: HttpClient) {
  }

  get() {
    return this.http.get<Author[]>('http://localhost:3000/authors').pipe(tap(entities => {
      this.authorStore.set(entities);
    }));
  }

}

UI Layer

We need to design the UI layer to show the initial list of blogs. To design our UI layer, we will split our UI into smart components also known as container components and presentation components (sometimes known as dumb components). We will start building the home screen of our application, which has the author filter section and blog section. Blog section is further split into blog listing section and an add blog section. This is the final screen output.

Final Output

author-section

<div>
    <span>
        Select User:
        <select class="form-control" (change)="onSelectAuthor($event.target.value)">
            <option *ngFor="let author of authors$ | async">{{author.name}}</option>
        </select>
    </span>
</div>

blog-section

<app-blog-list (deleteBlogEvent)="deleteBlog($event)" [blogs]="blogs$ | async"></app-blog-list>
<app-add-blog [filter]="filter" (addBlogEvent)="addBlog($event)"></app-add-blog>

blog-list

<div class="row">
    <div class="col-sm-6">
        <table class="table-striped">
            <thead>
                <tr>
                    <td>
                        <p> Title </p>
                    </td>
                    <td>
                        <p> Author</p>
                    </td>
                    <td></td>
                    <td align="right">
                        <p>Action</p>
                    </td>
                </tr>
            </thead>

            <tr *ngFor="let blog of blogs">
                <td class="col-sm-1">
                    {{blog.title}}
                </td>
                <td>
                    {{blog.author}}
                </td>
                <td class="col-sm-1">
                </td>
                <td align="right" class="col-sm-1">
                    <button class="btn-link" (click)="deleteBlog(blog)">Delete</button>
                </td>
            </tr>
        </table>
    </div>
</div>

The presentation components receive the data from smart components via @Input and the smart components receive any actions from the presentation components via @Output. In our case, blog-section is the main component and blog-list is our presentation component. author-section is the component that holds the author filter drop down.

First, we will load the authors to fill in the filter dropdown by calling the author service.

export class AuthorSectionComponent implements OnInit {

  @Output()
  updateFilter = new EventEmitter();
  authors$: Observable<Author[]>;
  constructor(private authorService: AuthorService, private filterService: FilterService, private authorQuery: AuthorQuery) { }

  ngOnInit() {
    this.authorService.get().subscribe();
    this.authors$ = this.authorQuery.selectAll();
  }

  onSelectAuthor(author: string) {
    this.updateFilter.emit(author);
    this.filterService.updateFilter(author === 'All' ? 'All' : author);
  }
}
this.authorService.get().subscribe();

This call above will set up author store with authors data. You will notice we are getting the authors$ data as an observable by calling store's selectAll() method. You can learn more about Akita's store query APIs here.
To load all the blogs we could have used the blog query and call just the selectAll() function.

this.blogs$ = this.blogQuery.selectAll();

But in our scenario, our application state changes whenever we update the filter, or when we add a new blog. RxJS has an operator called combinelatest() to achieve this functionality. So this is our updated code in blog-section.

   this.blogs$ = combineLatest(
      this.blogQuery.selectAll(),
      this.filterQuery.select(state => state.authorFilter.filter),
      (blogs: any, authorFilter: any) => {
        return blogs ? blogs.filter(blog => authorFilter === 'All' ? blog :   blog.author === authorFilter) : [];
      }

Any time we add a new blog to the store, or update the filter condition we will receive the latest values and we just need to apply the latest filter condition to the new array of blogs that we receive the from the combineLatest() function.

Conclusion

As you can see Akita is much simpler than NgRx in terms of boilerplate and integration with Angular. I find it super easy to implement when compared to NgRx, just use a service to set the store data and use a query inside the components to retrieve the data as an observable.

You can find the complete code here.

Top comments (0)