Throwing my 2c in here as I’ve gone down many paths of state management in Aurelia over the years. While tools like aurelia-store
, @aurelia/state
(if you’re using Aurelia 2) and Redux are awesome for some things, I’ve seen them overused, and sometimes they just add a bunch of extra work for not much gain.
Honestly, for a lot of projects, a simple singleton class can handle state just fine. You basically create a class that holds your app’s data, and then you can inject it anywhere you need it.
Here’s a quick example:
export class AppState {
someValue = 'initial value';
someOtherValue = 0;
updateSomeValue(newValue) {
this.someValue = newValue;
}
incrementOtherValue() {
this.someOtherValue++;
}
}
Super simple, right? You lose things like undo/redo and time-travel debugging, but let’s be real, how often do you actually need those?
If you do want the ability to track state, you can actually write some simple code to do this without additional libs or config:
export class AppState {
private history: any[] = [];
private historyIndex: number = -1;
public someValue: string = 'initial value';
constructor() {
this.saveState(); // Save initial state
}
updateSomeValue(newValue: string) {
this.someValue = newValue;
this.saveState();
}
private saveState() {
// Truncate history if we've undone and then made a new change
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push({ someValue: this.someValue });
this.historyIndex++;
}
canUndo() {
return this.historyIndex > 0;
}
undo() {
if (this.canUndo()) {
this.historyIndex--;
this.someValue = this.history[this.historyIndex].someValue;
}
}
canRedo() {
return this.historyIndex < this.history.length - 1;
}
redo() {
if (this.canRedo()) {
this.historyIndex++;
this.someValue = this.history[this.historyIndex].someValue;
}
}
}
To use it, you’d just inject it:
import { inject } from 'aurelia-framework';
import { AppState } from './app-state';
@inject(AppState)
export class MyComponent {
constructor(private appState: AppState) {}
updateValue() {
this.appState.updateSomeValue('new value!');
}
undo() {
this.appState.undo();
}
redo() {
this.appState.redo();
}
get canUndo() {
return this.appState.canUndo();
}
get canRedo() {
return this.appState.canRedo();
}
}
This only tracks one property, but it shows you how to implement these things inside a singleton. If you need advanced state tracking, you could throw in RxJS to make it a bit nicer. The aurelia-store
plugin in v1 uses RxJS and a BehaviorSubject
for state. So at that point you’d probably be better off using an Aurelia lib instead of rolling your own.
One HUGE thing: don’t use state management for temporary stuff like form data or UI state. Trust me, it’s a nightmare. Aurelia’s built-in data binding is perfect for that kind of thing.
I worked on a big Aurelia project where we went all-in on state management, and it ended up being a huge pain. We spent so much time just dealing with the extra complexity. We eventually ripped it out, and the codebase became way easier to manage. We switched to singletons (one for each data type) and then leveraged localStorage for caching, getters/setters for controlling access to the data and how it’s read and written.
So, before you jump into a full-on state management solution, think about if you really need it. Start simple, and only add the complexity if you absolutely have to. You might be surprised how far a simple singleton can take you.