action
Overview
Actions are simple classes that have a unique type
property and, optionally, additional information.
Actions generally take one of two forms:
- Command: A verb. For example,
[Theme] ToggleTheme
- Event: Past tense. For example,
[Library] BookSelected
When unsure whether to use a command or an event for a certain action, it is advisable to choose event.
Events simply relay what has already happened, without making assumptions about how the various states should react.
In contrast, a command could potentially obscure information about what has happened and dictate what states should do.
Defining an action
An action is required to have a unique type
. By convention, type
follows the format [<Context>] <ActionName>
to uniquely identify the action throughout the app.
import { ThemeName } from '../../graphql/types';
export class ToggleThemeAction {
public static readonly type = '[Theme] ToggleTheme';
public constructor(
public readonly force?: ThemeName
) { }
}
Dispatching an action
Dispatching an action is as simple as calling apollo.dispatch(new ToggleThemeAction())
from anywhere in the app.
It returns an Observable<void>
which completes when all states have completed handling the action. If any of the action
handlers throws an error, then the observable emits an error.
This can be useful for executing some logic only after an action has completed, especially if some of the handlers are asynchronous. For example:
export class ThemeComponent {
public constructor(
private readonly apollo: Apollo
) { }
protected toggleTheme(force?: ThemeName): void {
this.apollo.dispatch(new ToggleThemeAction(force)).subscribe({
next: () => console.log('Theme toggle succeeded'),
error: () => console.error('Theme toggle failed')
});
}
protected dispatchMultipleActions(): void {
forkJoin([
this.apollo.dispatch(new FirstAction()),
this.apollo.dispatch(new SecondAction()),
]).subscribe({
next: () => console.log('Actions execution succeeded'),
error: () => console.error('Actions execution failed')
});
}
}
Unlike, mutate
, dispatch
executes regardless of whether the observable is subscribed to or not.
API
public dispatch<TAction extends ActionInstance>(action: TAction): Observable<void>
type ActionInstance = InstanceType<ActionType<any>>;
interface ActionType<T> {
type: string;
new(...args: Array<any>): T;
}
Handling an action
action
is used in a state
slice to define an action handler.
An action
handler can be synchronous or asynchronous and can be used to update the cache, call a service, or perform any other logic.
Action handlers can be chained together to form a sequence of actions that are executed in order, by using the dispatch
function which is available in the ActionContext
parameter.
Usage
Let's look at an example of how a LogoutUserAction
can be handled asynchronously in the user state
and chained with another action handler.
export const userState = () => {
const userSessionApi = inject(UserSessionApi);
return state(descriptor => descriptor
.action(LogoutUserAction, (action, { dispatch }) => {
return userSessionApi.endUserSession().pipe(
mergeMap(() => dispatch(new UserLoggedOutAction()))
);
})
.action(UserLoggedOutAction, (action, { cache }) => {
cache.writeQuery({ ...new CurrentUserQuery(), data: { currentUser: null } });
})
);
}
When apollo.dispatch(new LogoutUserAction())
is called from a component or a service, it will:
- First, invoke the
LogoutUserAction
handler. - Then, invoke the
UserLoggedOutAction
handler. - Finally, complete the observable returned by
apollo.dispatch(new LogoutUserAction())
when both handlers have finished executing.
It is important to return the observable from an asynchronous action handler to ensure that the observable returned by dispatch
completes only after the action handler has finished executing.
API
type ActionFn<T> = (action: T, context: ActionContext) => void | Promise<any> | Observable<any>;
interface ActionContext<TCacheShape = any> {
cache: ApolloCache<TCacheShape>;
dispatch<TAction extends Action | ActionInstance>(action: TAction): Observable<void>;
}
In the next page we will see a complete example of how actions and their handlers can be used for managing local state.
Actions stream
Apollo
exposes an actions
stream which emits all dispatched actions at different stages of their lifecycle.
Apollo Orbit provides the following (fully typed) RxJS operators to filter the actions stream:
ofActionDispatched
: When an action is first dispatched.ofActionSuccess
: When execution of all action handlers has completed successfully.ofActionError
: When execution of one or more action handlers has failed.ofActionComplete
: When execution of all action handlers has completed, regardless of success or failure.
All of the operators return an Observable<Action>
except for ofActionComplete
which returns Observable<ActionComplete<Action>>
.
API
interface ActionComplete<TAction = any> {
action: TAction;
error?: Error;
status: 'success' | 'error';
}
Examples
this.apollo.actions.pipe(
ofActionDispatched(AddBookAction)
).subscribe({
next: action => console.log(`Add book '${action.book.id}' dispatched.`)
});
this.apollo.actions.pipe(
ofActionSuccess(AddBookAction)
).subscribe({
next: action => console.log(`Add book '${action.book.id}' succeeded.`)
});
this.apollo.actions.pipe(
ofActionError(AddBookAction)
).subscribe({
next: action => console.error(`Add book '${action.book.id}' failed.`)
});
this.apollo.actions.pipe(
ofActionComplete(AddBookAction)
).subscribe({
next: result => {
if (!result.error) {
console.log(`Add book '${result.action.book.id}' succeeded.`);
} else {
console.error(`Add book '${result.action.book.id}' failed. Error: ${result.error.message}`);
}
}
});
this.apollo.actions.pipe(
ofActionSuccess(BookSelectedAction, BookDeselectedAction)
).subscribe(action => {
if (action instanceof BookSelectedAction) {
console.log(`Book '${action.selectedId}' was selected`);
} else if (action instanceof BookDeselectedAction) {
console.log(`Book '${action.deselectedId}' was deselected`);
}
});
Unsubscribe logic omitted for brevity.
Actions stream can also be used for direct communication between components on the page, removing the need for a shared service in some scenarios.