Transactions

Note that in most cases you won't need them

Transactions are an optimization for performing multiple operations on the store.

Let's say we have the following store and query:

auth.store.ts
auth.query.ts
function createInitialState(): AuthState {
return {
firstName: '',
token: ''
};
}
@StoreConfig({ name: 'auth' })
class AuthStore extends Store<AuthState> {
constructor() {
super(createInitialState());
}
}
export class AuthQuery extends QueryEntity<AuthState> {
constructor(protected store: AuthStore) {
super(store);
}
}

And we want to query the entire state:

const authState$ = authQuery.select().subscribe()

Now let's say we need to update the same store a couple of times on the same tick:

update() {
this.store.update(..);
this.store.setLoading(true);
}

This will trigger the authState$ subscriber twice, something we want to avoid. In cases like these Akita's transactions come in handy - they ensures that a dispatch occurs only after all the store actions defined in the transaction have been called. We can use them as decorators, functions or operators:

import { transaction, applyTransaction, withTransaction } from '@datorama/akita';
@transaction()
update() {
this.store.update();
this.store.setLoading(true);
}
update() {
applyTransaction(() => {
this.store.update();
this.store.setActive(1);
});
}
update() {
return http.get().pipe(
withTransaction(response => {
...
})
)
}

Now the store will dispatch the new values only once, after the final update has finished.

Akita provides the combineQueries observable, which is useful in cases where we need to return data from our store, combined with data arriving from additional queries, like combineLatest. One example of this is combining data from other stores:

movies.query.ts
movies.service.ts
import { combineQueries } from '@datorama/akita';
export class MoviesQuery extends QueryEntity<MoviesState> {
constructor(protected store: MoviesStore,
private actorsQuery: ActorsQuery,
private genresQuery: GenresQ uery) {
super(store);
}
selectMovies() {
return combineQueries([
this.selectAll(),
this.actorsQuery.selectAll({ asObject: true }),
this.genresQuery.selectAll({ asObject: true })
]
)
.pipe(
map(([movies, actors, genres]) => {
return movies.map(movie => {
return {
...movie,
actors: movie.actors.map(actorId => actors[actorId]),
genres: movie.genres.map(genreId => genres[genreId])
};
});
})
);
}
}
export class MoviesService {
constructor(private moviesStore: MoviesStore,
private actorsStore: ActorsStore,
private genresStore: GenresStore,
private moviesQuery: MoviesQuery) {}
getMovies() {
return http.get().pipe(
mapTo(movies),
withTransaction(response => {
this.actorsStore.set(response.entities.actors);
this.genresStore.set(response.entities.genres);
const movies = {
entities: response.entities.movies,
ids: response.result
};
this.moviesStore.set(movies);
})
);
}
}

In our service, when we fetch the movies and update the store, we wrap it in a transaction, and in the query, when selecting the movies, we use the combineQueries operator in order to combine the movie data with the actors & genres data from the other stores. This will make sure that our subscribers will receive a single notification, instead of one per update.