Architecture

Let's say we are building from scratch an e-commerce application. We start by creating a new project with Angular CLI:

npx @angular/cli new appName

Next, we'll add Akita by using schematics:

ng add @datorama/akita

Session Feature

Now, we want to add a session module to our application, so we'll create a SessionModule:

ng g m session

Inside the session module we can create components such as LoginComponent and SignupComponent:

ng g c session/login
ng g c session/signup

Now, it's time to choose our Store. The rule is simple. If you don't need to manage a collection of entities, you should go with the basic store. In this case, we only have one user, so we need to create the basic store:

ng g af session/session --plain

The above command creates a SessionStore, SessionQuery, and a SessionService. So now our application tree is:

📦app
┣ 📂session
┃ ┣ 📂login
┃ ┃ ┣ 📜login.component.css
┃ ┃ ┣ 📜login.component.html
┃ ┃ ┣ 📜login.component.spec.ts
┃ ┃ ┗ 📜login.component.ts
┃ ┣ 📂state
┃ ┃ ┣ 📜session.query.ts
┃ ┃ ┣ 📜session.service.ts
┃ ┃ ┗ 📜session.store.ts
┃ ┗ 📜session.module.ts
┣ 📜app-routing.module.ts
┣ 📜app.component.css
┣ 📜app.component.html
┣ 📜app.component.spec.ts
┣ 📜app.component.ts
┗ 📜app.module.ts

Let's see each one of the files inside the state folder:

session.store.ts
session.query.ts
session.service.ts
import { Injectable } from '@angular/core';
import { Store, StoreConfig } from '@datorama/akita';
export interface SessionState {
key: string;
}
export function createInitialState(): SessionState {
return {
key: ''
};
}
@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'session' })
export class SessionStore extends Store<SessionState> {
constructor() {
super(createInitialState());
}
}
import { Injectable } from '@angular/core';
import { Query } from '@datorama/akita';
import { SessionStore, SessionState } from './session.store';
@Injectable({ providedIn: 'root' })
export class SessionQuery extends Query<SessionState> {
constructor(protected store: SessionStore) {
super(store);
}
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SessionStore } from './session.store';
import { tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class SessionService {
constructor(private sessionStore: SessionStore,
private http: HttpClient) {
}
}

Each one of the providers is marked as @Injectable({ providedIn: 'root' }) . It means that the store, the query, and the service are app-wide singletons, and therefore can be accessed everywhere in our application. For example, in components, directives, services, and queries.

Let's modify our session.store file and add the relevant properties:

session.store.ts
import { Injectable } from '@angular/core';
import { Store, StoreConfig } from '@datorama/akita';
export interface SessionState {
name: string | null;
token: string | null;
}
export function createInitialState(): SessionState {
return {
name: null,
token: null
};
}
@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'session' })
export class SessionStore extends Store<SessionState> {
constructor() {
super(createInitialState());
}
}

We recommend placing the logic for underlying queries inside the query class so it can be more readable and reusable:

session.query.ts
import { Injectable } from '@angular/core';
import { Query } from '@datorama/akita';
import { SessionStore, SessionState } from './session.store';
@Injectable({ providedIn: 'root' })
export class SessionQuery extends Query<SessionState> {
isLogin$ = this.select('token');
name$ = this.select('name');
constructor(protected store: SessionStore) {
super(store);
}
}

In the service, we'll make our server calls, and update the store:

session.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SessionStore } from './session.store';
import { tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class SessionService {
constructor(private sessionStore: SessionStore,
private http: HttpClient) {
}
login(creds) {
return this.http(endpoint).pipe(
tap(user => this.sessionStore.update(user))
)
}
}

Now, we can access the SessionQuery where we need it and design our views.

Products Feature

First, we need to create the ProductsModule and a ProductsPageComponent:

ng g m products
ng g c products/products-page

Next, we want to maintain a collection of products so we need to create an EntityStore:

ng g af products/products

The above command creates a ProductsStore, ProductsQuery, Product, and a ProductsService. So now our application tree is:

📦app
┣ 📂products
┃ ┣ 📂products-page
┃ ┃ ┣ 📜products-page.component.css
┃ ┃ ┣ 📜products-page.component.html
┃ ┃ ┣ 📜products-page.component.spec.ts
┃ ┃ ┗ 📜products-page.component.ts
┃ ┣ 📂state
┃ ┃ ┣ 📜product.model.ts
┃ ┃ ┣ 📜products.query.ts
┃ ┃ ┣ 📜products.service.ts
┃ ┃ ┗ 📜products.store.ts
┃ ┗ 📜products.module.ts
┣ 📂session
┃ ┣ 📂login
┃ ┃ ┣ 📜login.component.css
┃ ┃ ┣ 📜login.component.html
┃ ┃ ┣ 📜login.component.spec.ts
┃ ┃ ┗ 📜login.component.ts
┃ ┣ 📂state
┃ ┃ ┣ 📜session.query.ts
┃ ┃ ┣ 📜session.service.ts
┃ ┃ ┗ 📜session.store.ts
┃ ┗ 📜session.module.ts
┣ 📜app-routing.module.ts
┣ 📜app.component.css
┣ 📜app.component.html
┣ 📜app.component.spec.ts
┣ 📜app.component.ts
┗ 📜app.module.ts

Let's see each one of the files inside the state folder:

products.store
products.query
product.model
products.service
import { Injectable } from '@angular/core';
import { Product } from './product.model';
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
export interface ProductsState extends EntityState<Product> {}
@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'products' })
export class ProductsStore extends EntityStore<ProductsState> {
constructor() {
super();
}
}
import { Injectable } from '@angular/core';
import { QueryEntity } from '@datorama/akita';
import { ProductsStore, ProductsState } from './products.store';
@Injectable({ providedIn: 'root' })
export class ProductsQuery extends QueryEntity<ProductsState> {
constructor(protected store: ProductsStore) {
super(store);
}
} import { ID } from '@datorama/akita';
export interface Product {
id: number | string;
}
export function createProduct(params: Partial<Product>) {
return {
} as Product;
}
import { Injectable } from '@angular/core';
import { ID } from '@datorama/akita';
import { HttpClient } from '@angular/common/http';
import { ProductsStore } from './products.store';
import { Product } from './product.model';
import { tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class ProductsService {
constructor(private productsStore: ProductsStore,
private http: HttpClient) {}
get() {
return this.http.get<Product[]>('https://api.com').pipe(tap(entities => {
this.productsStore.set(entities);
}));
}
add(product: Product) {
this.productsStore.add(product);
}
update(id, product: Partial<Product>) {
this.productsStore.update(id, product);
}
remove(id: ID) {
this.productsStore.remove(id);
}
}

You should follow the same principles as with the Session feature.

Angular providers don't have to be wide app singletons, for more information read this article.

Join Queries

Queries can talk to other queries, join entities from different stores, etc. Let's say we have a products store and a cart store.

products.store.ts
product.model.ts
cart.store.ts
cart-item.model.ts
import { Product } from './products.model';
import { EntityState, EntityStore } from '@datorama/akita';
export interface ProductsState extends EntityState<Product> {}
@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'products' })
export class ProductsStore extends EntityStore<ProductsState> {
constructor() {
super();
}
}
export type Product = {
id: number;
title: string;
description: string;
price: number;
};
export interface CartState extends EntityState<CartItem> {}
@Injectable({ providedIn: 'root' })
@StoreConfig({
name: 'cart',
idKey: 'productId'
})
export class CartStore extends EntityStore<CartState> {
constructor() {
super();
}
}
export type CartItem = {
productId: Product['id'];
quantity: number;
total: number;
};

We need to show the list of cart items and the total amount, but we also need some information from the product, like the title and the price. Therefore we need to join the CartStore with the ProductsStore:

cart.query.ts
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CartQuery extends QueryEntity<CartState> {
constructor(protected store: CartStore,
private productsQuery: ProductsQuery) {
super(store);
}
selectItems$ = combineLatest([
this.selectAll(),
this.productsQuery.selectAll({ asObject: true })
]
).pipe(map(joinItems));
}
function joinItems([cartItems, products]: [CartItem[], Product[]]) {
return cartItems.map(item => {
const product = products[item.productId];
return {
...item,
...product,
total: item.quantity * product.price
};
});
}

We’re using the combineLatest() observable to get both the list of cart items and the products. Then we are mapping over them, merging a cart item with the corresponding product based on the productId.

You can find the complete tutorial here.

High-Level Principles:

1. The Store is a single object that contains the store state and serves as the “single source of truth.”

2. The only way to change the state is by calling setState() (or one of the update methods based on it).

3. A component should NOT get data from the store directly but instead use a Query:

A component doesn’t need to know its data source, so we recommend not to inject the store inside components, but have store updates pass-through service:

products-page.component
@Component({
selector: 'app-products-page',
templateUrl: './products-page.component.html'
})
export class ProductsPageComponent {
products$ = this.productsQuery.selectAll();
constructor(
private productsService: ProductsService,
private productsQuery: ProductsQuery) { }
addProduct(product) {
this.productsService.add(product);
}
}

The component only should know how to get the data, and therefore it injects the Query.

This rule isn't written in stone. Another valid approach is to encapsulate both the Store and the Query behind a Service and use it, for example:

products-page.component
products.service
@Component({
selector: 'app-products-page',
templateUrl: './products-page.component.html'
})
export class ProductsPageComponent {
products$ = this.productsService.query.selectAll();
constructor(private productsService: ProductsService) { }
}
@Injectable({ providedIn: 'root' })
export class ProductsService {
constructor(private productsStore: ProductsStore,
public query: ProductsQuery,
private http: HttpClient) {
}
}

4. Asynchronous logic and update calls should be encapsulated in services and data services.

Using Mediators