listeners
This is the shared documentation for the actionOn and thunkOn APIs. Both APIs allow you to declare a listener, which gets executed in response to configured actions or thunks having been executed.
The actionOn API allows you to declare a listener which will be used to update state, similar to a standard action. It shares many characteristics of actions, so we recommend that you are familiarise yourself with actions first.
The thunkOn API allows you to declare a listener which will be used to perform side effects, or logic based action dispatching, similar to a standard thunk. It shares many characteristics of a thunk, so we recommend that you are familiarise yourself with thunks first.
Whilst actionOn and thunkOn are similar to action and thunk respectively they do have the following distinctions:
- They require you define a
targetResolver
function as the first argument to your listener definitions. ThetargetResolver
will receive the store actions and is responsible for resolving the target(s) to listen to. - The handler for the action/thunk listener will receive a
target
argument instead of apayload
argument. Thistarget
argument is an object containing a lot of useful information about the target being handled, including the payload. - They will not be provided via the store's
getActions
API, or via theactions
argument that are provided to thunks. A store instance does however expose agetListeners
API which would allow you to manually trigger listeners - although we recommend that you strictly use this for testing only.
Let's explore a brief example of each of the listener APIs.
actionOn
A listener action is responsible for performing updates to state in response to configured targets being executed.
onAddTodo: actionOn(
// targetResolver:
actions => actions.addTodo,
// handler:
(state, target) => {
state.auditLog.push(`Added a todo: ${target.payload}`);
}
)
thunkOn
A listener thunk is responsible for performing side effects (e.g. a call to an HTTP endpoint), or logic based action dispatching, in response to configured targets being executed.
onAddTodo: thunkOn(
// targetResolver:
actions => actions.addTodo,
// handler:
async (actions, target) => {
await auditService.add(`Added a todo: ${target.payload}`);
}
)
targetResolver
The first argument you provide to a listener definition is the targetResolver
function.
onAddTodo: thunkOn(
actions => actions.addTodo, // 👈 the targetResolver function
async (actions, target) => {
await auditService.add(`Added a todo: ${target.payload}`);
}
)
This function will receive the following arguments:
actions
(Object)The local actions relating to where your listener is bound on your model.
storeActions
(Object)All of the actions for the entire store.
The function should then resolve one of the following:
An action
actions => actions.addTodo
A thunk
actions => actions.saveTodo
The explicit string type of the action to listen to
actions => 'ROUTE_CHANGED'
An array of actions/thunks/strings
actions => [ actions.saveTodo, 'ROUTE_CHANGED' ]
target
Argument
Instead of a payload
argument, listeners recieve a target
argument.
onAddTodo: thunkOn(
actions => actions.addTodo,
// The target argument
// 👇
async (actions, target) => {
await auditService.add(`Added a todo: ${target.payload}`);
}
)
The target
argument is an object containing the following properties:
type
(string)The type of the target action that is being responded to. e.g.
"@actions.todos.addTodo"
. This can be helpful as a mechanism of distinguishing if you are targeting multiple actions.payload
(any)This will contain the same payload that the target action received.
result
(any | null)When listening to a thunk, if the thunk succeeded and returned a result, the result will be contained within this property.
error
(Error | null)When listening to a thunk, if the thunk failed, this property will contain the
Error
.resolvedTargets
(Array<string>)An array containing a list of the targets resolved by the
targetResolver
function. This aids in performing target based logic within a listener handler. We will provide an example of this below.
Listening to multiple actions
A listener can have multiple targets. To aid having logic specific to each target type, both the actionOn
and thunkOn
handlers will receive a resolvedTargets
array within the target
argument.
The resolvedTargets
array contains the list of the action types resolved by the targetResolver
function you provided to the listener. This array will match the index order of the types resolved by the targetResolver
.
const auditModel = {
logs: [],
onCriticalAction: actionOn( // Resolved targets, by array index
(actions, storeActions) => [ // 👇
storeActions.session.loggedIn, // 👈 0
storeActions.session.loggedOut, // 👈 1
storeActions.todos.addedTodo, // 👈 2
],
(state, target) => {
// The target argument will additionally contain a "resolvedTargets"
// property, being an array containing all the types of the resolved
// targets. This allows you to easily pull out these type references and
// then perform target based logic against them.
// Note how the array index of each type matches the array index as
// defined in the targetResolver function above.
// 👇 0 👇 1 👇 2
const [loggedIn, loggedOut, addedTodo] = target.resolvedTargets;
// The target current being handled
// 👇
switch (target.type) {
case loggedIn: \\...
case loggedOut: \\...
case addedTodo: \\...
}
}
)
}
Listening to specific stages of a thunk
By default when you resolve a thunk as a target your listener will be triggered when the respective thunk succeeds or fails.
It is possible to target specific stages of a thunk (e.g. start, success, fail) when defining your listeners.
The action creators (i.e. the action instances used to dispatch an action with) have their "types" bound against them as properties.
For example, given the following store model;
const storeModel = {
todos: {
items: [],
addTodo: action(() => ...),
saveTodo: thunk(() => ...)
}
};
const store = createStore(storeModel);
We can access the type of each action/thunk like so;
// actions:
console.log(store.getActions().todos.addTodo.type); // @action.todos.addTodo
// thunks:
console.log(store.getActions().todos.saveTodo.startType); // @thunk.todos.saveTodo(start)
console.log(store.getActions().todos.saveTodo.successType); // @thunk.todos.saveTodo(success)
console.log(store.getActions().todos.saveTodo.failType); // @thunk.todos.saveTodo(fail)
As you can see thunks have multiple types, each representing an action which will be fired each for each stage of an action. These actions have no effect on state and simply act as mechanism by which to respond to specific stages of a thunk. Each of the types can be described as follows:
startType
Represents an action that is fired when the thunk has started
successType
Represents an action that is fired when the thunk has succeeded (i.e. no errors)
failType
Represents an action that is fired when the thunk has failed
Using this refactoring you can configure your listeners to target a specific action stage of a thunk, for example;
onAddTodo: actionOn(
actions => actions.saveTodo.successType, // 👈 only targeting thunk success
(state, target) => {
state.auditLog.push(`Successfully saved a todo: ${target.payload}`);
}
)