Dispatching an action is the process of triggering it to update the state. You use the store.dispatch()
method to dispatch actions.
The dispatch()
method takes at least one argument:
- The action to dispatch: This can be either the action function itself or the name of the action if it was registered with a name using
store.registerAction()
. - Optional arguments: Any additional arguments you provide to
dispatch()
will be passed to the action function.
// app.ts
import { inject } from 'aurelia-dependency-injection';
import { Store } from 'aurelia-store';
import { addFramework } from './actions'; // Import the action
import { State } from './state';
@inject(Store)
export class App {
private state: State;
constructor(private store: Store<State>) {
this.store.registerAction('Add Framework', addFramework);
this.store.state.subscribe((newState) => {
this.state = newState;
});
}
addNewFramework(name: string) {
this.store.dispatch(addFramework, name);
// Or: this.store.dispatch('Add Framework', name);
}
}
Explanation:
this.store.dispatch(addFramework, name);
: This line dispatches theaddFramework
action, passing thename
argument to it.this.store.dispatch('Add Framework', name);
: This line does the same thing but uses the registered name of the action instead of the action function itself.
If you dispatch an asynchronous action, and you need to perform operations that depend on the state being updated after the action has completed, you should await
the dispatch
call:
async addNewFramework(name: string) {
await this.store.dispatch(addFramework, name);
console.log('Framework added:', this.state.frameworks); // This will log the updated state
}
By awaiting dispatch
, you ensure that the subsequent code will only execute after the asynchronous action has finished and the state has been updated.
Sometimes, you might want to dispatch multiple actions in sequence to achieve a specific state update. While you could call dispatch()
multiple times, this can lead to unnecessary intermediate state updates and make it harder to track the overall change in debugging tools. The Aurelia Store plugin provides a way to pipe multiple actions together and dispatch them as a single unit using store.pipe()
.
The store.pipe()
method allows you to chain multiple actions together. It returns a PipedDispatch
object, which has a dispatch()
method to execute the entire pipeline.
// app.ts
import { inject } from 'aurelia-dependency-injection';
import { Store } from 'aurelia-store';
import { addFramework, setFrameworks, State } from './actions';
@inject(Store)
export class App {
constructor(private store: Store<State>) {
this.store.registerAction('Add Framework', addFramework);
this.store.registerAction('Set Frameworks', setFrameworks);
}
addAndSetFrameworks(newFramework: string, allFrameworks: string[]) {
this.store
.pipe(addFramework, newFramework)
.pipe(setFrameworks, allFrameworks)
.dispatch();
}
}
Explanation:
this.store.pipe(addFramework, newFramework)
: This starts a new pipeline by adding theaddFramework
action with thenewFramework
argument..pipe(setFrameworks, allFrameworks)
: This adds thesetFrameworks
action with theallFrameworks
argument to the pipeline..dispatch();
: This executes the entire pipeline as a single unit.
- Single State Transition: Piping actions ensures that only a single state update is emitted at the end of the pipeline, even if multiple actions are involved.
- Atomic Updates: The state update appears as a single, atomic operation in debugging tools like the Redux DevTools.
- Improved Readability: Piping can make complex state updates more readable and easier to understand.
In the Redux DevTools, piped actions will be displayed in the format actionA->actionB->actionC
, clearly indicating the sequence of actions that were executed.
If any action in the pipeline throws an error or returns a promise that rejects, the rest of the pipeline will not be executed, and the error will be propagated. If you are awaiting the dispatch()
of a piped action, you will receive the error there.
Sometimes, you might not have direct access to the Store
instance, especially in child components that are designed to be reusable and independent of the specific state management implementation. In such cases, the dispatchify
higher-order function can be a valuable tool.
dispatchify
is a function that takes an action as an argument and returns a new function. This new function, when called, will automatically obtain the Store
instance and dispatch the provided action with any arguments passed to it. It essentially "wraps" your action, making it easier to dispatch from components that don't have a direct reference to the store.
Let's say you have a framework-list
component that displays a list of frameworks and a framework-item
component that represents a single framework and has a button to add a new one. You want to keep the framework-item
component "dumb" or presentational, meaning it doesn't know anything about the store or how actions are dispatched.
actions.ts
:
import { State } from './state';
export const addFramework = (state: State, frameworkName: string) => {
const newState = Object.assign({}, state);
newState.frameworks = [...newState.frameworks, frameworkName];
return newState;
};
app.ts
:
import { inject } from 'aurelia-dependency-injection';
import { Store } from 'aurelia-store';
import * as actions from './actions';
import { State } from './state';
@inject(Store)
export class App {
constructor(private store: Store<State>) {
this.store.registerAction('Add framework', actions.addFramework);
}
}
app.html
:
<template>
<require from="./framework-list"></require>
<framework-list></framework-list>
</template>
framework-list.ts
:
import { bindable } from 'aurelia-framework';
import { connectTo, dispatchify } from 'aurelia-store';
import * as actions from './actions';
import { State } from './state';
@connectTo()
export class FrameworkList {
@bindable state: State;
addFramework: (name: string) => Promise<void>;
constructor() {
// Wrap the addFramework action with dispatchify
this.addFramework = dispatchify(actions.addFramework);
}
}
framework-list.html
:
<template>
<require from="./framework-item"></require>
<framework-item add.bind="addFramework"></framework-item>
<ul>
<li repeat.for="framework of state.frameworks">${framework}</li>
</ul>
</template>
framework-item.ts
:
import { bindable } from 'aurelia-framework';
export class FrameworkItem {
@bindable add: (name: string) => Promise<void>;
newFrameworkName: string;
addFramework() {
this.add(this.newFrameworkName);
this.newFrameworkName = ''; // Clear the input field
}
}
framework-item.html
:
<template>
New framework name:
<input value.bind="newFrameworkName" />
<button click.trigger="addFramework()">Add</button>
</template>
Explanation:
- In
framework-list.ts
, we usedispatchify(actions.addFramework)
to create a new functionaddFramework
. This function is now ready to be passed down to child components. - In
framework-item.ts
, theadd
function (which is theaddFramework
function passed from the parent) is called directly from the template when the button is clicked. Theframework-item
component doesn't need to know anything about the store or how to dispatch actions.
This approach allows you to clearly separate your components into:
- Presentational (or "dumb") components: These components are only concerned with how things look. They receive data and callbacks (like the
add
function) via properties and don't interact with the store directly.framework-item
is an example. - Container (or "smart") components: These components are aware of the store and handle the interaction with it. They connect to the state, dispatch actions, and pass data and callbacks down to presentational components.
framework-list
andApp
are examples.