action
Overview
Actions are simple objects that have a unique type
property and, optionally, additional information.
Actions generally take one of two forms:
- Command: A verb. For example,
theme/toggle
- 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 domain/action
to uniquely identify the action throughout the app.
import { ThemeName } from '../../graphql';
export interface ToggleThemeAction {
type: 'theme/toggle';
force?: ThemeName;
}
Dispatching an action
The useDispatch()
hook returns a reference to a dispatch
function that can be used to dispatch actions. For example:
import { useDispatch } from '@apollo-orbit/react';
import { useQuery } from '@apollo/client';
import { ThemeDocument } from './graphql';
import { ToggleThemeAction } from './states/theme/theme.actions';
export function Theme() {
const dispatch = useDispatch();
const { data: themeData } = useQuery(ThemeDocument);
return (
<div>
<span>Current theme:</span>
<b>{themeData?.theme.displayName}</b>
<button onClick={() => dispatch<ToggleThemeAction>({ type: 'theme/toggle' })}>Toggle theme</button>
</div>
);
}
dispatch
returns a Promise<void>
which completes when all states have completed handling the action. If any of the action
handlers throws an error, then the promise is rejected with the error. This can be useful for executing some logic only after an action has completed, especially if some of the handlers are asynchronous.
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, execute a side-effect, 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 = state(descriptor => descriptor
.action<LogoutUserAction>('user/logout', async (action, { dispatch }) => {
await userSessionApi.endUserSession();
return dispatch<UserLoggedOutAction>({ type: 'user/logged-out' });
})
.action<UserLoggedOutAction>('user/logged-out', (action, { cache }) => {
cache.writeQuery({ query: CurrentUserDocument, data: { currentUser: null } });
})
);
When dispatch<LogoutUserAction>({ type: 'user/logout' })
is called from a component, it will:
- First, invoke the
LogoutUserAction
handler. - Then, invoke the
UserLoggedOutAction
handler. - Finally, resolve the promise returned by
dispatch<LogoutUserAction>({ type: 'user/logout' })
when both handlers have finished executing.
It is important to return the promise from an asynchronous action handler to ensure that the promise returned by dispatch
completes only after the action handler has finished executing.
API
type ActionFn<T> = (action: T, context: ActionContext) => void | Promise<any>;
interface Action {
type: string;
}
interface ActionContext<TCacheShape = any> {
cache: ApolloCache<TCacheShape>;
dispatch<TAction extends Action>(action: TAction): Promise<void>;
}
In the next page we will see a complete example of how actions and their handlers can be used for managing local state.