Building a todo app with state management
Learn state management in Aurelia by building a todo application with @aurelia/state
This tutorial walks you through building a todo application using the @aurelia/state plugin. You'll learn how to manage application state centrally, handle actions, use middleware, and persist data—all while building a real, functioning app.
What You'll Learn
By the end of this tutorial, you'll understand:
How to install and configure
@aurelia/stateCreating and managing global state
Writing action handlers to update state
Binding templates to state with
.stateand.dispatchUsing the
@fromStatedecorator in componentsAdding middleware for logging and persistence
Memoizing expensive derived state computations
Integrating Redux DevTools for debugging
Prerequisites
Before starting, you should be familiar with:
Basic TypeScript or JavaScript
What We're Building
A todo application with these features:
Add, complete, and delete todos
Filter todos by status (all, active, completed)
Display todo statistics (total, active, completed)
Persist todos to localStorage
Undo/redo support via Redux DevTools
Step 1: Create the Project
Use the Aurelia CLI to scaffold a new project:
When prompted, choose TypeScript (recommended for better type safety with state).
Navigate into your project:
Step 2: Install @aurelia/state
Install the state management plugin:
Step 3: Define Your State Shape
Create a new file src/state/app-state.ts to define your application state structure:
This defines a clear structure for our application state. Everything that needs to be shared across components or persisted will live here.
Step 4: Create Action Handlers
Action handlers are pure functions that take the current state and an action, then return a new state. They're similar to reducers in Redux.
Create src/state/action-handlers.ts:
Key principles:
Action handlers are pure functions—no side effects
Always return a new state object instead of mutating the existing one
Use the spread operator (
...state) to preserve unchanged propertiesReturn the original state if the action doesn't apply
Step 5: Create Middleware
Middleware intercepts actions before or after they're processed. Let's create logging and persistence middleware.
Create src/state/middleware.ts:
How middleware works:
Middleware with
placement: 'before'runs before action handlersMiddleware with
placement: 'after'runs after action handlersReturn
falseto block an action from proceedingReturn
undefinedor the state to continue processing
Step 6: Configure the State Plugin
Now register the state plugin in your app. Update src/main.ts:
Configuration breakdown:
mergedState: Combines initial state with persisted data from localStoragemiddlewares: Registers our logging and persistence middlewaredevToolsOptions: Enables Redux DevTools integration (install the browser extension to use it)todoActionHandler: Registers our action handler
Step 7: Create the Todo Input Component
Create src/components/todo-input.ts:
Create src/components/todo-input.html:
Key concepts:
value.state="newTodoText": Binds input value to thenewTodoTextproperty in global stateinput.dispatch="...": Dispatches an action on every input event to update stateresolve(IStore): Injects the state store using Aurelia's dependency injection
Step 8: Create the Todo List Component
Create src/components/todo-list.ts:
Create src/components/todo-list.html:
Advanced concepts:
@fromState(selector): Automatically keeps component properties in sync with statecreateStateMemoizer(): Optimizes performance by caching computed valuesSelectors only recalculate when their dependencies change (by reference)
Step 9: Create the Main App Component
Update src/my-app.ts:
Update src/my-app.html:
Step 10: Add Styling
Create or update src/my-app.css:
Step 11: Run Your App
Start the development server:
Your browser should open automatically. Try:
Adding new todos
Toggling todo completion
Deleting todos
Switching between filters (all, active, completed)
Refreshing the page (todos persist via localStorage)
Step 12: Debug with Redux DevTools
Install the Redux DevTools browser extension:
Open the extension and you'll see:
Action History: Every action dispatched in your app
State Inspector: Current state at any point in time
Time Travel: Jump back and forth through state changes
State Diff: See exactly what changed between states
Try dispatching actions and watch them appear in real-time!
Understanding the Data Flow
Here's how state flows through your application:
User Action: User types in input or clicks a button
Dispatch: Component calls
store.dispatch({ type: 'ACTION_NAME', ... })Before Middleware: Logging middleware logs the action
Action Handler:
todoActionHandlerprocesses the action and returns new stateAfter Middleware: Persistence middleware saves to localStorage
State Update: Store updates its internal state
Subscriber Notification: All
@fromStateproperties automatically updateTemplate Re-render: Aurelia updates the DOM with new values
Best Practices
1. Keep State Normalized
Instead of nested structures, keep data flat:
2. Use Memoized Selectors for Expensive Computations
3. Keep Action Handlers Pure
Action handlers should be pure functions with no side effects:
Put side effects (API calls, etc.) in your components or middleware instead.
4. Use TypeScript for Type Safety
Define action types to catch errors at compile time:
Next Steps
Now that you've built a todo app with state management, explore:
State Outcome Recipes: Patterns for optimistic updates, rollback, and more
State Plugin Guide: Complete reference documentation
Middleware: Advanced middleware patterns
Testing: Learn to test components that use state (coming soon)
Common Questions
When should I use @aurelia/state vs local component state?
Use @aurelia/state when:
Data needs to be shared across multiple components
You need a single source of truth
You want time-travel debugging
You need to persist state between sessions
Use local component state when:
Data is only used within one component
State is ephemeral (like UI state)
You want simpler, lighter-weight code
Can I use @aurelia/state with the router?
Yes! State management works great with routing. You might store the current route, route parameters, or data loaded for specific routes in your global state.
How does this compare to Redux?
@aurelia/state is inspired by Redux but simplified:
Action handlers combine reducers and action creators
Middleware works the same way
Redux DevTools integration is built-in
Less boilerplate required
Can I dispatch actions from templates?
Yes! Use the .dispatch binding command:
This is convenient for simple actions. For complex logic, dispatch from your component instead.
Summary
You've built a complete todo application with centralized state management! You learned:
✅ Installing and configuring @aurelia/state ✅ Defining state shape and initial state ✅ Writing action handlers to update state ✅ Using .state and .dispatch in templates ✅ Decorating properties with @fromState ✅ Creating middleware for logging and persistence ✅ Optimizing with memoized selectors ✅ Debugging with Redux DevTools
The patterns you learned here scale to applications of any size. As your app grows, you can split action handlers into multiple files, add more middleware, and create reusable selectors.
Happy state managing!
Last updated
Was this helpful?