Keeping state with a Service using Signals

Signals is the newest and shiny thing in Angular 16, and often it is compared with observables or how we are going to use RxJS.

In this article, I will tackle creating a service to store state with signals since this is one of the most common uses of the BehaviorSubject as you can see in some of the articles below:

Angular State Management With BehaviorSubjectAngular service to handle state using BehaviorSubject

Creating the service

The goal of this service is to keep any kind of state in any shape and make it available for multiple components. Also, we want to make it easy to modify the state and provide easy access to each of the properties.

We will start by defining the class that other services will extend in order to define the state they want to store. This class uses the signalstate, and it is initialized with an empty object of type T

export class SignalsSimpleStoreService<T> {
state = signal({} as T);

constructor() {}

}

Now, let’s work on how the state is set.

For this, we will have two methods that use the updatemethod inside the signal.

set is used to set a single property of the statesetState to update a partial or whole state./**
* This is used to set a new value for a property.
*
* @param key – the key of the property to be set
* @param data – the new data to be saved
*/
public set<K extends keyof T>(key: K, data: T[K]) {
this.state.update((currentValue) => ({ …currentValue, [key]: data }));
}

/**
* Sets values for multiple properties on the store.
* This is used when there is a need to update multiple
* properties in the store
*
* @param partialState – the partial state that includes
* the new value to be saved
*/
public setState(partialState: Partial<T>): void {
this.state.update((currentValue) => ({ …currentValue, …partialState }));
}

The next step is to create an easy way to access not only the whole state but a specific property of it. To achieve this, we will use the computed since it derives a new reactive value from an expression:

/**
* Returns a reactive value for a property on the state.
* This is used when the consumer needs the signal for
* specific part of the state.
*
* @param key – the key of the property to be retrieved
*/
public select<K extends keyof T>(key: K): Signal<T[K]> {
return computed(() => this.state()[key]);
}

Here is the complete implementation:

export class SignalsSimpleStoreService<T> {
readonly state = signal({} as T);

constructor() {}

/**
* Returns a reactive value for a property on the state.
* This is used when the consumer needs the signal for
* specific part of the state.
*
* @param key – the key of the property to be retrieved
*/
public select<K extends keyof T>(key: K): Signal<T[K]> {
return computed(() => this.state()[key]);
}

/**
* This is used to set a new value for a property
*
* @param key – the key of the property to be set
* @param data – the new data to be saved
*/
public set<K extends keyof T>(key: K, data: T[K]) {
this.state.update((currentValue) => ({ …currentValue, [key]: data }));
}

/**
* Sets values for multiple properties on the store
* This is used when there is a need to update multiple
* properties in the store
*
* @param partialState – the partial state that includes
* the new value to be saved
*/
public setState(partialState: Partial<T>): void {
this.state.update((currentValue) => ({ …currentValue, …partialState }));
}
}

Using the service

In the example application, we have a component that shares state with other child components, and we want to make modifications to one of the child components and see the changes in all the other components.

Shows an application with multiple components that need to update and read state

To achieve this, we will create a new service that extends the SignalsSimpleStoreService and we pass the UserState as the type:

export interface UserState {
name: string;
company: string;
address: string;
}

@Injectable()
export class UserSignalsStateService extends SignalsSimpleStoreService<UserState> {
constructor() {
super();
}
}

You may have noticed that the service was not provided at the application root; this is because we plan to set it as a provider in the parent component, restricting access to the shared state only to this component and its children.

@Component({
selector: ‘demos-signals-simple-store’,
standalone: true,
imports: [
CommonModule,
PageContentComponent,
ChangeNameComponent,
ChangeCompanyComponent,
ChangeAddressComponent,
],
templateUrl: ‘./signals-simple-store.component.html’,
styleUrls: [‘./signals-simple-store.component.scss’],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [UserSignalsStateService], // 👈 Providing the service
})

Now to read from the store, we could read a single attribute by using the select method from the user state service:

@Component({

changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChangeNameComponent {

name = this.userState.select(‘name’); // 👈 Reading a slice of the state

constructor(private userState: UserSignalsStateService) {}
}

Also, we can read the whole state from the service like this:

@Component({

providers: [UserSignalsStateService],
})
export class SignalsSimpleStoreComponent implements OnInit {

// 👇 Reading the whole state
readonly user = this.userSignal.state.asReadonly();

constructor(private userSignal: UserSignalsStateService) {}
}

To render values, we do it as is normally done with signals:

<div>
<p class=”text-2xl”>Current State</p>
</div>
<div class=”flex-col mt-5″>
<p class=”text-indigo-600 font-semibold”>
Name: <span>{{ user().name }}</span>
</p>
<p class=”text-indigo-600 font-semibold”>
Company:<span>{{ user().company }}</span>
</p>
<p class=”text-indigo-600 font-semibold”>
Address: <span>{{ user().address }}</span>
</p>
</div>
</div>

Finally, to update, we can use the setState to update a partial part of the state or use the set to modify just one property.

changeName() {
const newName = faker.name.fullName();

// 👇 Updating partial or whole state
this.userState.setState({ name: newName } as UserState);
}

changeAddress() {
const newAddress = faker.address.streetAddress(true);

// 👇 Updating a slice of the state
this.userState.set(‘address’, newAddress);
}

Conclusion

So hopefully this will help you identify other use cases for signals and how we can create a service to keep them.

You can always use the NgRx Component Store to share states between components, but maybe you want to bootstrap your own service to fit your business rules or standardize.

You can see the code for this article here:

ng-demos/signals-simple-store.service.ts at main · alfredoperez/ng-demos

You can find me on Twitter and on my website.

Keeping state with a Service using Signals was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Comment

Your email address will not be published. Required fields are marked *