State
A guide on working with the Aurelia State plugin.
This guide aims to help you become familiar with the Aurelia State plugin, a means of managing the state in your Aurelia applications.
When should you use state management?
Before we delve too deeply into Aurelia State and how it can help manage state in your Aurelia applications, we should discuss when to use state management and when not to.
When you need to reuse data in other parts of your application — State management shines when it comes to helping keep your data organized for cross-application reuse.
When dealing with complex data structures — Ephermal state is great for simple use cases. Still, when working with complex data structures (think multi-step forms or deeply structured data), state management can help keep it consistent.
Scenario driven guides
Ready-made patterns such as optimistic updates, persistence, and replay debugging live in the state outcome recipes. Start there when you need an end-to-end solution.
Aurelia State guides
Installing Aurelia State
To install the Aurelia State plugin, open up a Command Prompt/Terminal and install it:
npm i @aurelia/stateSetup the initial state
When registering the Aurelia State plugin, you must pass in your application's initial state. This is an object which defines the data structure of your application.
Create a new file in the src directory called initialstate.ts with your state object inside:
As you can see, it's just a plain old Javascript object. In your application, your properties would be called something different, but you can see we have a mixture of empty values and some defaults.
This state will be stored in the global state container for bindings to use in the templates.
Setup the action handlers
The initial state above isn't meant to be mutated directly. In order to produce a state change, a mutation request should be dispatched instead. An action handler is a function that is supposed to combine the current state of the state container and the action parameters of the function call to produce a new state:
An example of an action handler that produces a new state with the updated keyword on a keyword action:
Create a new file in the src directory called action-handlers.ts with the above code.
Configuration
To use the Aurelia State plugin in Aurelia, it needs to be imported and registered. Inside main.ts, the plugin can be registered as follows:
The above imports the StateDefaultConfiguration object from the plugin and then calls init, passing the initial state object and action handlers for your application.
For advanced use cases with middleware, the second parameter can be an options object:
Template binding
With .state and .dispatch commands
.state and .dispatch commandsThe Aurelia State Plugin provides state and dispatch binding commands that simplify binding with the global state. Example usages:
With & state binding behavior
& state binding behaviorIn places where it's not possible to use the .state binding command, the global state can be connected via & state binding behavior, like the following example:
Accessing view model
Note: by default, bindings created from .state and .dispatch commands will only allow you to access the properties on the global state. If it's desirable to access the property of the view model containing those bindings, use $parent like the following example:
Promise and Observable Support
The .state binding command supports asynchronous data through Promises and RxJS-style observables:
Promise Support
Observable Support
The plugin also supports RxJS-style observables:
Key Features:
Promises are resolved once and the resolved value is bound
Observables continuously update the bound value
Cleanup functions are called when components are destroyed
Works with both
.statecommand and& statebinding behavior
View model binding
With @fromState decorator
@fromState decoratorSometimes, it's also desirable to connect a view model property to the global state. The Aurelia State Plugin supports this via the @fromState decorator. An example usage is as follows:
With the above, whenever the state changes, it will ensure the keywords property of the view model stays in sync with the keywords property on the global state.
Memoizing derived state
Expensive computations in @fromState selectors will run on every state change by default. To avoid unnecessary work, the createStateMemoizer helper allows you to memoize derived values so they are recomputed only when their dependencies actually change.
In the example above, the selectTotal function executes only when items changes by reference. Other state updates won't trigger a recalculation, keeping the component performant and giving derived logic a clear place to live.
When you only need to read a value from state or perform a cheap calculation, passing a simple function directly to @fromState is usually adequate. The decorated property will update on every state change, which keeps things straightforward.
createStateMemoizer shines when deriving data is expensive or shared across multiple components. Because the selector remembers its last inputs, recalculation happens only when those inputs change by reference. This reduces wasted work and centralizes complex logic.
Here is another example using multiple selectors:
In contrast, writing the filter inline like @fromState(s => s.items.filter(i => i.includes(s.search))) would rerun on every single state update, even when neither items nor search changed.
Authoring action handlers
As mentioned at the start of this guide, action handlers are the way to handle mutation of the global state. They are expected to return to a new state instead of mutating it. Even though normal mutation works, it may break future integration with devtool.
Action handlers can be either synchronous or asynchronous. An application may have one or more action handlers, and if one action handler is asynchronous, a promise will be returned for the dispatch call.
An action handler should return the existing state (first parameters) if the action is unnecessary.
Middleware
Middleware provides a way to intercept and process actions before or after they reach your action handlers. This is particularly useful for cross-cutting concerns such as logging, validation, authentication, error handling, and performance monitoring.
What is middleware?
Middleware functions are executed before or after actions are processed by action handlers. Middleware can be either synchronous or asynchronous. When a middleware returns a Promise, the store will await it and use the resolved value (if any) before continuing the dispatch pipeline. This makes it possible to perform tasks such as API calls, logging to remote endpoints, reading from IndexedDB, etc. without blocking the UI thread.
Middleware can:
Log actions for debugging and auditing
Validate actions before processing
Transform or modify actions
Handle authentication and authorization
Monitor performance
Process state after action handlers complete
Creating middleware
A middleware function has the following signature:
Here's a simple logging middleware example:
And an example of an asynchronous middleware:
Middleware return values
Middleware can return different values to control execution:
Return state object: Continue execution with the returned state
Return
undefinedorvoid: Continue with the current state unchangedReturn
false: Block the action from proceeding further
Registering middleware
Middleware is registered during state plugin configuration using the MiddlewarePlacement enum to specify when the middleware should run:
Middleware placement
Use MiddlewarePlacement to control when middleware executes:
MiddlewarePlacement.Before: Runs before action handlersMiddlewarePlacement.After: Runs after action handlers
Middleware execution order
Middleware functions are executed in the order they are registered within their placement group:
Common middleware patterns
1. Validation middleware
2. Authentication middleware
3. Action transformation middleware
4. Error handling middleware
5. Performance monitoring middleware
Middleware with settings
Middleware can accept custom settings through the third parameter:
Middleware factory pattern
For more advanced scenarios, you can create factory functions that return middleware:
Runtime middleware management
You can add and remove middleware at runtime using the store instance:
Best practices for middleware
Keep middleware focused: Each middleware should have a single responsibility
Handle errors gracefully: Use try-catch blocks and return appropriate values
Consider performance: Avoid heavy computations in frequently executed middleware
Use TypeScript: Leverage type safety for better development experience
Order matters: Register middleware in the correct execution order for your use case
Return appropriate values: Use
falseto block actions, return state to continueTest thoroughly: Write unit tests for your middleware functions
Consider async cost: Async middleware is supported but will delay the completion of
dispatchuntil thePromisesettles. Keep any network / long-running work minimal or move it off the critical path when possible.
Debugging middleware
When troubleshooting middleware issues, you can add debug middleware to trace execution:
Runtime middleware management
You can add and remove middleware at runtime using the store instance:
Redux DevTools Integration
The Aurelia State plugin includes built-in support for Redux DevTools, providing powerful debugging capabilities for state management:
Automatic DevTools Connection
DevTools integration is enabled automatically when the Redux DevTools extension is installed:
DevTools Configuration Options
For advanced DevTools configuration, you can provide options:
DevTools Features Available
Time Travel Debugging: Navigate through state changes
Action Inspection: View dispatched actions and their payloads
State Inspection: Deep inspection of state at any point in time
Action Filtering: Filter actions by type or content
State Export/Import: Save and load state snapshots
Example of type declaration for application stores
Applications can enforce strict typings with view model-based dispatch calls via the 2nd type parameter of the store.
For example, if the store only accepts two types of actions, that has the following type:
Then the store can be declared like this:
Last updated
Was this helpful?