|
| 1 | +--- |
| 2 | +title: Angular State Management for 2025 |
| 3 | +slug: angular-state-management-2025 |
| 4 | +authors: ['Mike Hartington'] |
| 5 | +tags: [angular] |
| 6 | +cover_image: /blog/images/2024-12-16/thumbnail.png |
| 7 | +--- |
| 8 | + |
| 9 | +## Revisiting State Management |
| 10 | + |
| 11 | +The topic of state management in apps is something developers can spend countless hours discussing and never agree on what the "right" solution is. Truth is, there are so many solutions out there these days and they all share similar concepts that you can't really pick wrong. This is true in most frontend frameworks, but especially true in Angular, where the framework has introduced several new features in the past year that make managing state simpler than ever. Let's take a look at some approaches to state management in Angular that take advantage of modern features of the framework and can set us up for success in 2025. |
| 12 | + |
| 13 | +## Signals Only, No Library |
| 14 | + |
| 15 | +In recent versions of Angular, the framework has introduced a new primitive called Signals. Signals provide a way to store a value, update that value, and react to when that value changes. This sounds like features that you would get from a full featured state management library. The benefit of using Signals without any additional libraries is that you can make things work for you how ever you want. If you have opinions on how you want to construct your state and manage updates, raw Signals can accommodate this. |
| 16 | + |
| 17 | +```ts |
| 18 | +@Component({ |
| 19 | + template: ` |
| 20 | + <p>Hello, {{ name() }}</p> |
| 21 | + <button (click)="updateName()">Update</button> |
| 22 | + `, |
| 23 | +}) |
| 24 | +export class MessageComponent { |
| 25 | + name = signal('World'); |
| 26 | + constructor() { |
| 27 | + effect(() => { |
| 28 | + console.log('Name has changed: ', this.name()); |
| 29 | + }); |
| 30 | + } |
| 31 | + updateName() { |
| 32 | + this.name.set('Mike'); |
| 33 | + } |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +The built-in methods on the `signal` make creating a simple local store very easy to do. If you take Signals and utilize a service, you create your own mini-store. |
| 38 | + |
| 39 | +```ts |
| 40 | +export class AppStore { |
| 41 | + readonly state = signal([]); |
| 42 | + |
| 43 | + add(item) { |
| 44 | + this.state.update((oldState) => [...oldState, item]); |
| 45 | + } |
| 46 | + delete(item) { |
| 47 | + this.state.update((oldState) => oldState.filter((e) => e.id !== item.id)); |
| 48 | + } |
| 49 | + update(item) { |
| 50 | + this.state.update((oldState) => |
| 51 | + oldState.map((e) => (e.id === item.id ? item : e)) |
| 52 | + ); |
| 53 | + } |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +Now this is just a very basic approach to managing state, but if you do not want to reach for an additional libray, you could get pretty far with this basic solution. Throw a few `effect`s in there if you need to perform some side effects, and you're golden. |
| 58 | + |
| 59 | +## Signal State |
| 60 | + |
| 61 | +If you've been around Angular long enough, you've probably reached for NgRx before, and with good reason. NgRx provides a standard way of managing state in your app that is scalable and testable. In the past, NgRx has provided a store solution based on RxJS, but in more recent releases, NgRx provides two new API based on Signals, Signal State and Signal Store. |
| 62 | + |
| 63 | +Signal State is a lightweight API meant to be used in smaller, more isolated scenarios, where a full redux-like API isn't needed. This could be in small to medium sized apps, and in the component itself or extracted to a service. |
| 64 | + |
| 65 | +Reworking our previous example, we can take our signal-based store and update it to use Signal State: |
| 66 | + |
| 67 | +```ts |
| 68 | +import { patchState, signalState } from '@ngrx/signals'; |
| 69 | +export class AppStore { |
| 70 | + readonly state = signalState<Store>({ items: [] }); |
| 71 | + |
| 72 | + addToStore(item: StoreItem) { |
| 73 | + patchState(this.state, (oldState) => ({ |
| 74 | + ...oldState, |
| 75 | + items: [...oldState.items, item], |
| 76 | + })); |
| 77 | + } |
| 78 | + removeFromStore(item: StoreItem) { |
| 79 | + patchState(this.state, (oldState) => ({ |
| 80 | + ...oldState, |
| 81 | + items: oldState.items.filter((e) => e.id !== item.id), |
| 82 | + })); |
| 83 | + } |
| 84 | + updateStore(item: StoreItem) { |
| 85 | + patchState(this.state, (oldState) => ({ |
| 86 | + ...oldState, |
| 87 | + items: oldState.items.map((e) => |
| 88 | + e.id === item.id ? { ...item, name: 'bar' } : e |
| 89 | + ), |
| 90 | + })); |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +Instantly, some things should stand out to you. First, we use the new `signalState` function, instead of the raw `signal` API. Now we can have a more type safe mechanism for interacting with our state. Second, we're no long passing an array. `signalState` only accepts an object/record like value. If you need an array, you put that on a property of the state. |
| 96 | + |
| 97 | +Finally, the way we interact with our state is different. Instead of manipulating the state directly, we use the `patchState` function instead. `patchState` takes the state we want to manipulate, and uses a function to return a new version of that state. To add a new item to our `items` object, we can simply use the spread operator. Removing an item means we use `filter`, and updating an item means we use `map`. What's great about this is not only are we doing things in an immutable way, we're also getting all the types from our state. If we pass along a type that our state doesn't recognize, we'll get a type error before we even save. |
| 98 | + |
| 99 | +## Signal Store |
| 100 | + |
| 101 | +So Signal State is a more prescriptive way of handling smaller state, be it in a component or service. What is Signal Store all about? Signal Store is the more robust solution that you would expect for NgRx. It still is based on Signals, keeping the structure that most larger teams would want for this state solutions. Again, let's rework our previous example and update it for Signal Store. |
| 102 | + |
| 103 | +```ts |
| 104 | +const AppStore = signalStore( |
| 105 | + withState<Store>({ |
| 106 | + items: [], |
| 107 | + }), |
| 108 | + withMethods((state) => ({ |
| 109 | + addToStore(item: StoreItem) { |
| 110 | + patchState(state, (oldState) => ({ |
| 111 | + ...oldState, |
| 112 | + items: [...oldState.items, item], |
| 113 | + })); |
| 114 | + }, |
| 115 | + removeFromStore(item: StoreItem) { |
| 116 | + patchState(state, (oldState) => ({ |
| 117 | + ...oldState, |
| 118 | + items: oldState.items.filter((e) => e.id !== item.id), |
| 119 | + })); |
| 120 | + }, |
| 121 | + updateStore(item: StoreItem) { |
| 122 | + patchState(state, (oldState) => ({ |
| 123 | + ...oldState, |
| 124 | + items: oldState.items.map((e) => |
| 125 | + e.id === item.id ? { ...item, name: 'bar' } : e |
| 126 | + ), |
| 127 | + })); |
| 128 | + }, |
| 129 | + })) |
| 130 | +); |
| 131 | +``` |
| 132 | + |
| 133 | +Here, we're starting to see a more structured approach to managing state that isolates our state interactions from the rest of our app. Instead of creating a service, we use the `signalStore` function instead. `signalStore` will return an injectable service instead that we provide to a component, or our root app instance. From here, we pass a `withState` function to provide any actual state value to the store. Like `signalState`, this is an object/record only. |
| 134 | + |
| 135 | +For modifying our store, we can use the `withMethods` function and pass any methods we want to expose to our app. What stands out here is that our store's value is accessible without having to inject it. Similar to `signalState`, we use the `patchState` to make any changes we need. Since the mechanism to modify the store in `signalStore` is very close to what we had in `signalState`, it's very approachable when migrating from your simple local store to something more full featured. So if your app and team grow significantly, this is a great path forward. |
| 136 | + |
| 137 | +## Parting Thoughts |
| 138 | + |
| 139 | +If you've tried managing state using something like NgRx or other redux-inspired APIs, signal-based solutions are a breath of fresh air. Whether you are just building a small app and just want to use the raw signal API, or if you are in a large enterprise and want a structured approach to managing things, Signal State or Signal Store are both excellent solutions. Check out the Angular docs on the Signals or NgRx's docs on Signal State or Signal Store |
| 140 | + |
| 141 | +- [Signals](https://angular.dev/essentials/signals) |
| 142 | +- [Signal State](https://ngrx.io/guide/signals/signal-state) |
| 143 | +- [Signal Store](https://ngrx.io/guide/signals/signal-store) |
| 144 | + |
| 145 | +Also, make sure to check out: |
| 146 | + |
| 147 | +- [Nx Docs](https://www.notion.so/getting-started/intro) |
| 148 | +- [X/Twitter](https://twitter.com/nxdevtools) |
| 149 | +- [LinkedIn](https://www.linkedin.com/company/nrwl/) |
| 150 | +- [Bluesky](https://bsky.app/profile/nx.dev) |
| 151 | +- [Nx GitHub](https://github.com/nrwl/nx) |
| 152 | +- [Nx Official Discord Server](https://go.nx.dev/community) |
| 153 | +- [Nx Youtube Channel](https://www.youtube.com/@nxdevtools) |
| 154 | +- [Speed up your CI](/nx-cloud) |
0 commit comments