Skip to main content

Transactions

Transactions are an optimization for performing multiple operations on the store. Let's say we have the following store and query:

auth.store.ts
function createInitialState(): AuthState {
return {
firstName: '',
token: ''
};
}

@StoreConfig({ name: 'auth' })
class AuthStore extends Store<AuthState> {

constructor() {
super(createInitialState());
}
}
auth.query.ts
export class AuthQuery extends query<AuthState> {

constructor(protected store: AuthStore) {
super(store);
}
}

And we want to query the entire state:

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

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

update(token: string) {
this.store.update({ token });
this.store.setLoading(false);
}

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 => {
this.store.update(response);
this.store.setActive(1);
})
)
}

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

tip

Transaction also works when updating multiple stores. For example:

queryOne.select().subscribe(value => {
const fromOtherQuery = queryTwo.getValue();
})

When updating both stores inside a transaction, you'll have a guarantee that the value of both will be up to date inside the query selector.

combineQueries

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
import { combineQueries } from '@datorama/akita';

export class MoviesQuery extends QueryEntity<MoviesState> {

constructor(protected store: MoviesStore,
private actorsQuery: ActorsQuery,
private genresQuery: GenresQuery) {
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])
};
});
})
);
}
}
movies.service.ts
import { withTransaction } from '@datorama/akita';

export class MoviesService {
constructor(private moviesStore: MoviesStore,
private actorsStore: ActorsStore,
private genresStore: GenresStore) {}

getMovies() {
return http.get().pipe(
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.