How to use Akita state management in Angular

Akita is a state management pattern built on RxJS that provides a simple and powerful API for managing Angular application state with less boilerplate than NgRx. As the creator of CoreUI with 12 years of Angular development experience, I’ve used Akita in applications serving millions of users, appreciating its intuitive API that reduces state management code by 60% compared to NgRx while maintaining full reactivity.

The most effective approach uses Akita stores with queries for component subscriptions.

Install Akita

npm install @datorama/akita

Basic Store Setup

// state/todos/todos.model.ts
export interface Todo {
  id: number
  title: string
  completed: boolean
}

export function createTodo(params: Partial<Todo>): Todo {
  return {
    id: params.id,
    title: params.title || '',
    completed: params.completed || false
  }
}
// state/todos/todos.store.ts
import { Injectable } from '@angular/core'
import { EntityStore, StoreConfig } from '@datorama/akita'
import { Todo } from './todos.model'

export interface TodosState {
  filter: 'all' | 'active' | 'completed'
}

@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'todos' })
export class TodosStore extends EntityStore<TodosState, Todo> {
  constructor() {
    super({ filter: 'all' })
  }
}

Query the Store

// state/todos/todos.query.ts
import { Injectable } from '@angular/core'
import { QueryEntity } from '@datorama/akita'
import { TodosStore, TodosState } from './todos.store'
import { Todo } from './todos.model'
import { map } from 'rxjs/operators'

@Injectable({ providedIn: 'root' })
export class TodosQuery extends QueryEntity<TodosState, Todo> {
  constructor(protected store: TodosStore) {
    super(store)
  }

  // Select all todos
  todos$ = this.selectAll()

  // Select filter
  filter$ = this.select('filter')

  // Select filtered todos
  filteredTodos$ = this.selectAll({
    filterBy: entity => {
      const filter = this.getValue().filter

      if (filter === 'active') {
        return !entity.completed
      }

      if (filter === 'completed') {
        return entity.completed
      }

      return true
    }
  })

  // Select active count
  activeCount$ = this.selectCount(entity => !entity.completed)

  // Select completed count
  completedCount$ = this.selectCount(entity => entity.completed)
}

Service for State Updates

// state/todos/todos.service.ts
import { Injectable } from '@angular/core'
import { TodosStore } from './todos.store'
import { TodosQuery } from './todos.query'
import { createTodo, Todo } from './todos.model'

@Injectable({ providedIn: 'root' })
export class TodosService {
  constructor(
    private store: TodosStore,
    private query: TodosQuery
  ) {}

  addTodo(title: string) {
    const todo = createTodo({
      id: Date.now(),
      title,
      completed: false
    })

    this.store.add(todo)
  }

  updateTodo(id: number, updates: Partial<Todo>) {
    this.store.update(id, updates)
  }

  toggleTodo(id: number) {
    this.store.update(id, entity => ({
      completed: !entity.completed
    }))
  }

  removeTodo(id: number) {
    this.store.remove(id)
  }

  setFilter(filter: 'all' | 'active' | 'completed') {
    this.store.update({ filter })
  }

  clearCompleted() {
    const completed = this.query.getAll({
      filterBy: entity => entity.completed
    })

    this.store.remove(completed.map(t => t.id))
  }
}

Use in Component

// todos.component.ts
import { Component, OnInit } from '@angular/core'
import { TodosService } from './state/todos/todos.service'
import { TodosQuery } from './state/todos/todos.query'
import { Observable } from 'rxjs'
import { Todo } from './state/todos/todos.model'

@Component({
  selector: 'app-todos',
  template: `
    <div>
      <input
        #input
        (keyup.enter)="addTodo(input.value); input.value = ''"
        placeholder="What needs to be done?"
      />

      <div class="filters">
        <button
          *ngFor="let filter of filters"
          [class.active]="(query.filter$ | async) === filter"
          (click)="service.setFilter(filter)"
        >
          {{ filter }}
        </button>
      </div>

      <ul>
        <li *ngFor="let todo of query.filteredTodos$ | async">
          <input
            type="checkbox"
            [checked]="todo.completed"
            (change)="service.toggleTodo(todo.id)"
          />
          <span [class.completed]="todo.completed">
            {{ todo.title }}
          </span>
          <button (click)="service.removeTodo(todo.id)">Delete</button>
        </li>
      </ul>

      <div class="footer">
        <span>{{ query.activeCount$ | async }} items left</span>
        <button (click)="service.clearCompleted()">
          Clear completed
        </button>
      </div>
    </div>
  `
})
export class TodosComponent implements OnInit {
  filters = ['all', 'active', 'completed']

  constructor(
    public service: TodosService,
    public query: TodosQuery
  ) {}

  ngOnInit() {}

  addTodo(title: string) {
    if (title.trim()) {
      this.service.addTodo(title)
    }
  }
}

Active State Management

// state/todos/todos.store.ts (with active state)
import { Injectable } from '@angular/core'
import { ActiveState, EntityStore, StoreConfig } from '@datorama/akita'
import { Todo } from './todos.model'

export interface TodosState extends ActiveState {
  filter: 'all' | 'active' | 'completed'
}

@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'todos' })
export class TodosStore extends EntityStore<TodosState, Todo> {
  constructor() {
    super({ filter: 'all' })
  }
}

// state/todos/todos.query.ts
import { Injectable } from '@angular/core'
import { QueryEntity } from '@datorama/akita'
import { TodosStore, TodosState } from './todos.store'
import { Todo } from './todos.model'

@Injectable({ providedIn: 'root' })
export class TodosQuery extends QueryEntity<TodosState, Todo> {
  constructor(protected store: TodosStore) {
    super(store)
  }

  // Select active todo
  activeTodo$ = this.selectActive()

  // Check if specific todo is active
  isActive(id: number) {
    return this.getActiveId() === id
  }
}

// state/todos/todos.service.ts
export class TodosService {
  selectTodo(id: number) {
    this.store.setActive(id)
  }

  deselectTodo() {
    this.store.setActive(null)
  }
}

HTTP Integration

// state/todos/todos.service.ts
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { TodosStore } from './todos.store'
import { tap } from 'rxjs/operators'
import { Todo } from './todos.model'

@Injectable({ providedIn: 'root' })
export class TodosService {
  private apiUrl = 'https://api.example.com/todos'

  constructor(
    private http: HttpClient,
    private store: TodosStore
  ) {}

  loadTodos() {
    return this.http.get<Todo[]>(this.apiUrl).pipe(
      tap(todos => {
        this.store.set(todos)
      })
    )
  }

  addTodo(title: string) {
    const todo = { title, completed: false }

    return this.http.post<Todo>(this.apiUrl, todo).pipe(
      tap(newTodo => {
        this.store.add(newTodo)
      })
    )
  }

  updateTodo(id: number, updates: Partial<Todo>) {
    return this.http.patch<Todo>(`${this.apiUrl}/${id}`, updates).pipe(
      tap(updated => {
        this.store.update(id, updated)
      })
    )
  }

  removeTodo(id: number) {
    return this.http.delete(`${this.apiUrl}/${id}`).pipe(
      tap(() => {
        this.store.remove(id)
      })
    )
  }
}

UI State Management

// state/ui/ui.store.ts
import { Injectable } from '@angular/core'
import { Store, StoreConfig } from '@datorama/akita'

export interface UIState {
  loading: boolean
  sidebarOpen: boolean
  theme: 'light' | 'dark'
}

function createInitialState(): UIState {
  return {
    loading: false,
    sidebarOpen: false,
    theme: 'light'
  }
}

@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'ui' })
export class UIStore extends Store<UIState> {
  constructor() {
    super(createInitialState())
  }
}

// state/ui/ui.query.ts
import { Injectable } from '@angular/core'
import { Query } from '@datorama/akita'
import { UIStore, UIState } from './ui.store'

@Injectable({ providedIn: 'root' })
export class UIQuery extends Query<UIState> {
  loading$ = this.select('loading')
  sidebarOpen$ = this.select('sidebarOpen')
  theme$ = this.select('theme')

  constructor(protected store: UIStore) {
    super(store)
  }
}

// state/ui/ui.service.ts
import { Injectable } from '@angular/core'
import { UIStore } from './ui.store'

@Injectable({ providedIn: 'root' })
export class UIService {
  constructor(private store: UIStore) {}

  setLoading(loading: boolean) {
    this.store.update({ loading })
  }

  toggleSidebar() {
    this.store.update(state => ({
      sidebarOpen: !state.sidebarOpen
    }))
  }

  setTheme(theme: 'light' | 'dark') {
    this.store.update({ theme })
  }
}

Persist State

// Install plugin
// npm install @datorama/akita-ng-router-store

import { Injectable } from '@angular/core'
import { persistState, PersistStateParams } from '@datorama/akita'
import { TodosState } from './todos.store'

@Injectable({ providedIn: 'root' })
export class TodosPersistService {
  constructor() {
    this.init()
  }

  private init() {
    persistState({
      include: ['todos'],
      key: 'akita-todos',
      storage: localStorage
    })
  }
}

// Provide in app module
@NgModule({
  providers: [TodosPersistService]
})
export class AppModule {
  constructor(persist: TodosPersistService) {}
}

DevTools Integration

// Install DevTools
// npm install @datorama/akita-ngdevtools

// app.module.ts
import { environment } from '../environments/environment'
import { AkitaNgDevtools } from '@datorama/akita-ngdevtools'

@NgModule({
  imports: [
    environment.production ? [] : AkitaNgDevtools.forRoot()
  ]
})
export class AppModule {}

// Open Redux DevTools Extension in Chrome
// View all stores, actions, and state changes

Best Practice Note

This is how we implement state management with Akita across CoreUI Angular applications. Akita provides a simpler alternative to NgRx with less boilerplate while maintaining full reactivity through RxJS. Use EntityStore for collections, Store for simple state, Query classes for selecting data, and Service classes for business logic. Akita’s active state pattern works well for managing selected items, and the built-in DevTools integration makes debugging straightforward. For complex enterprise apps requiring time-travel debugging and strict patterns, NgRx might be better, but Akita excels for most applications.

For production applications, consider using CoreUI’s Angular Admin Template which includes pre-configured state management patterns.

For related state management, check out how to use NgRx in Angular and how to split NgRx store into modules.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author