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/state

  • Creating and managing global state

  • Writing action handlers to update state

  • Binding templates to state with .state and .dispatch

  • Using the @fromState decorator in components

  • Adding middleware for logging and persistence

  • Memoizing expensive derived state computations

  • Integrating Redux DevTools for debugging

Prerequisites

Before starting, you should be familiar with:

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 properties

  • Return 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 handlers

  • Middleware with placement: 'after' runs after action handlers

  • Return false to block an action from proceeding

  • Return undefined or 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 localStorage

  • middlewares: Registers our logging and persistence middleware

  • devToolsOptions: 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 the newTodoText property in global state

  • input.dispatch="...": Dispatches an action on every input event to update state

  • resolve(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 state

  • createStateMemoizer(): Optimizes performance by caching computed values

  • Selectors 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:

  1. User Action: User types in input or clicks a button

  2. Dispatch: Component calls store.dispatch({ type: 'ACTION_NAME', ... })

  3. Before Middleware: Logging middleware logs the action

  4. Action Handler: todoActionHandler processes the action and returns new state

  5. After Middleware: Persistence middleware saves to localStorage

  6. State Update: Store updates its internal state

  7. Subscriber Notification: All @fromState properties automatically update

  8. Template 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:

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?