[](https://www.npmjs.com/package/rsm-signal-state-management) [](LICENSE)
npm install rsm-signal-state-managementshell
npm install rsm-signal-state-management
`
If you've ever worked with state management in Angular applications, you're likely familiar with NgRx. NgRx is a popular and robust library for managing state in Angular applications using Redux-inspired principles. However, what if I told you there's a simpler and more powerful alternative? Meet RSM Signal State Management.
Introduction
RSM Signal State Management is a state management library that leverages Angular signals to provide a straightforward and efficient way to manage the state of your application. It offers an elegant and flexible solution that simplifies complex state management scenarios. You won't need to create Actions and Reducers for each state you want to update. Also you don't need to create Selectors to have access to the states values.
RSM is a state management library built specifically for Angular. It offers a set of classes and functions designed to handle various aspects of state management with ease.
Here's an overview of the Generic classes of RSM Signal State Management:
1- PublicRsmPrimitiveGenericClass: Provides a generic class for managing primitive state types such as booleans, strings, and numbers.
2- PublicRsmEntityGenericClass: Handles complex state types like arrays and objects also the simple Primitives.
3- PublicRsmQueueGenericClass: Designed for creating queue systems, which are useful for handling modal systems, notifications, and more.
4- PublicRsmStackGenericClass: Allows you to manage state as a stack, enabling you to handle stack operations efficiently.
5- PublicRsmActionsGeneric: Provides a mechanism to create a message bus for dispatching and listening to actions within your application.
Consider a typical Angular application with multiple entities, each having its own set of states. For instance, you might have a user entity with states like profile, balance, and personal settings, as well as a product entity with states like info and availability.
$3
At the heart of this system lies a generic class, a powerhouse for managing application state. It serves as the backbone for handling, updating, and sharing data across your application. Here it is the most simple generic class:
`typescript
import { signal, computed, Signal, WritableSignal } from '@angular/core';
// Define a type for the store state with keys to track changes.
type StoreStateWithKeys = {
// lastUpdatedKeys is an array which keeps last updates state property keys, when user updates a single key or bunch of keys, this array will be updated.
lastUpdatedKeys: Array | undefined;
state: StatesModel;
};
// Create a class for managing a generic state using Angular signals.
export class PublicRsmPrimitiveGenericClass {
// Private state to hold the data. This must pe private to prevent user write new value directly into the state properties.
private readonly privateState: WritableSignal> = signal({
lastUpdatedKeys: undefined,
state: {} as StatesModel,
});
// Public signal for external components to access the state.
readonly store: Signal> = computed(() => {
return this.privateState();
});
// Constructor to initialize the state with initial values.
constructor(initialValues: StatesModel) {
this.setAllStates(initialValues); // Set initial state
}
// Select a specific property from the state.
public select(statePropertyKey: K): Signal {
return computed(() => this.privateState().state[statePropertyKey]);
}
// Expose a readonly state properties.
readonly state: Signal = computed(() => {
return this.privateState().state;
});
// Set a state property in the store. you need to pass the statePropertyKey that shows which state property you wish to update, and the data which is the updating value for that state property
// Method overloads to support different use cases
public updateState(
statePropertyKey: K,
data: StatesModel[K]
): void;
public updateState(
statePropertyKey: K,
data: Partial[K]
): void;
// Implementation combining the logic of both previous functions
public updateState(
statePropertyKey: K,
data: StatesModel[K] | Partial[K]
): void {
const objectType = Object.prototype.toString.call(data);
this.privateState.update((currentValue) => ({
...currentValue,
lastUpdatedKeys: [statePropertyKey],
state: { ...currentValue.state, [statePropertyKey]: objectType === '[object Object]'? { ...data }: data },
}));
}
// Set all properties in the store.
public setAllStates(allStates: StatesModel): void {
const keys = Object.keys(allStates) as Array;
this.privateState.update((currentValue) => ({
...currentValue,
lastUpdatedKeys: keys,
state: { ...allStates },
}));
}
}
`
The journey begins by defining a state model. This model acts as a blueprint for the data you intend to manage within your application. It's essential to have a clear understanding of your data structure from the outset.
#### StoreStateWithKeys:
lastUpdatedKeys: An array that keeps track of the keys (or properties) within the state that have seen recent updates.
state: An object representing the actual application state.
#### Safeguarding State Data
Data integrity is a priority, and to protect against direct manipulation, we establish a private state object named privateState. This object, of type WritableSignal> , acts as a secure container housing both the state data and the lastUpdatedKeys array.
Initially, the state is set as an empty object ({}), with lastUpdatedKeys left undefined. External access to this private state is tightly controlled, allowing read and modification only through designated functions.
#### Public Access via Signals
To provide external components and services access to the state, we introduce a public signal known as store. This signal offers a read-only perspective of the state residing within privateState. It ensures that external parts of the application can observe the state while preventing direct alterations.
#### Initialization via Constructor
Upon creating a service instance, you pass in initial values for your state. The constructor, in turn, triggers setAllStates(initialValues) to initialize the state with the provided data.
#### Selecting Specific State Properties
Accessing individual state properties is made possible through the select function. It returns a signal representing a specific property of the state, enabling you to track changes in specific data within the state.
#### Modifying State
To enact changes in the state, you rely on the updateState function. This function takes a key (property name) and the new data value to assign to that key. It's designed to handle various data types, ensuring the state remains consistent.
Furthermore, the setAllStates function allows for simultaneous updates of multiple properties, accepting an object containing all the new values.
#### A Unified Store
Beneath the surface, a single privateState object is responsible for housing the entire state management. Regardless of whether you're updating one or multiple properties, all changes are funneled through this central store. This approach ensures that modifications are coordinated and maintain consistency across the application.
$3
RSM Signal State Management offers a solution that's both simple and powerful. You can structure your states in a way that suits your application's needs. For example, you can have separate state management for user and product entities or create a main state to consolidate them all in one place. Let's take a closer look.
`typescript
export interface UserState {
profile: UserProfile | undefined;
balance: UserBalance |undefined;
personalSettings: UserPersonalSettings | undefined;
}
export interface ProductState {
info: ProductInfo | undefined;
availability: ProductAvailability | undefined;
}
export const initialUserState: UserState {
profile: undefined,
balance: undefined,
personalSettings: undefined
}
export const initialProductState: ProductState {
info: undefined,
availability: undefined,
}
/You can also, create a main state like this:/
export interface MainState {
user: UserState | undefined;
product: ProductState | undefined;
}
export const initialMainState: MainState {
user: undefined,
product: undefined,
}
`
$3
You also gonna need a service which extends one of the above generic classes from the library regarding what kind of state you want to create.
`typescript
import { Injectable } from '@angular/core';
import { PublicRsmEntityGenericClass } from 'projects/rsm-signal-state-management/src/lib/generic-classes/public-rsm-entity-generic';
@Injectable({
providedIn: 'root'
})
export class UserStoreService extends PublicRsmEntityGenericClass{
constructor() {
super(initialUserState);
}
}
`
$3
`typescript
userStoreService = inject(UserStoreService);
setUserProfileData(userProfile: UserProfile) {
// 'profile' is the interface key
this.userStoreService.updateState('profile', userProfile);
}
// To get access to user profile data to use in html you can easily use the select function and pass the property key
userProfileSignal: Signal = this.userStoreService.select('profile');
`
Usage
Now, let's explore how to use RSM Signal State Management in your Angular application through various examples.
$3
First, create your state model and initialize it, then, create a service that extends the PublicRsmPrimitiveGenericClass to manage primitive state types. This class offers methods for setting, getting, and observing state changes.
`typescript
export interface UserDetailsState {
username: string;
userIsLoggedIn: boolean;
userAge: number;
}
export const initialUserDetailsState: UserDetailsState = {
username: '',
userIsLoggedIn: true,
userAge: 0
};
import { Injectable } from '@angular/core';
import { PublicRsmPrimitiveGenericClass } from 'rsm-signal-state-management/src/lib/generic-classes/public-rsm-primitive-generic';
@Injectable({
providedIn: 'root'
})
export class UserDetailsStoreService extends PublicRsmPrimitiveGenericClass {
constructor() {
super(initialUserDetailsState);
}
}
`
Now use the service to update the states:
`typescript
// Import the service
const userDetailsStoreService = inject(UserDetailsStoreService);
// Example of set the 'username' property
updateUsername() {
userDetailsStoreService.updateState('username', 'Test Username');
}
// Example of set the 'userIsLoggedIn' property
updateLoginState() {
userDetailsStoreService.updateState('userIsLoggedIn', true);
}
// Access the state
const userDetailState: Signal = userDetailsStoreService.state;
`
$3
To manage more complex state types, such as arrays and objects, create a service that extends PublicRsmEntityGenericClass. Here's an example of managing an array of users:
`typescript
export interface UsersState {
users: User[];
userProfile: UserProfile | undefined;
}
export const initialUsersState: UsersState = {
users: [],
userProfile: undefined
}
`
`typescript
import { Injectable } from '@angular/core';
import { PublicRsmEntityGenericClass } from 'rsm-signal-state-management';
@Injectable({
providedIn: 'root'
})
export class UsersStoreService extends PublicRsmEntityGenericClass {
constructor() {
super(initialUsersState); // Initialize
}
}
`
`typescript
usersStoreService = inject(UsersStoreService);
addNewUser(user: User) {
this.usersStoreService.addItemToEndOfArray('users', user);
}
updateUserProfile(userProfile: UserProfile) {
this.usersStoreService.updateState('userProfile', userProfile);
}
removeUserById(userId: string) {
this.usersStoreService.removeFromArrayByProperty('users', 'id', userId);
}
`
$3
Creating a queue system for managing modal dialogs or notifications becomes straightforward with RSM Signal State Management. Here's an example of managing a notification queue:
`typescript
export interface NotificationsState {
notifs: Notification[];
currentNotif: Notification | undefined;
}
export const initialNotificationsState: NotificationsState = {
notifs: [],
currentNotif: undefined
}
`
`typescript
import { Injectable } from '@angular/core';
import { PublicRsmQueueGenericClass } from 'rsm-signal-state-management';
@Injectable({
providedIn: 'root'
})
export class NotificationStoreService extends PublicRsmQueueGenericClass {
constructor() {
super(initialNotificationsState);
}
}
`
`typescript
notifsStoreService = inject(NotificationsStoreService);
currentNotification: Signal = this.notifsStoreService.select('currentNotif');
notificationsQueue: Signal = this.notifsStoreService.select('notifs');
addNewNotification(notif: Notification) {
// If currently showing a notif, then add the new notif to the queue
if (this.currentNotification()) {
this.notifsStoreService.addToQueue('notifs', notif);
} else {
// If there are no notifs displaying then add the new notif to the current notif
this.notifsStoreService.updateState('currentNotif', notif);
}
}
// Fetch a notif from notif queue and show it
showNewNotifFromQueue() {
if (this.notificationsQueue.length !==0) {
const pickedNotif: Signal = this.notifsStoreService.removeFromQueue('notifs');
this.notifsStoreService.updateState('currentNotif', pickedNotif);
}
}
`
$3
Managing state as a stack is useful for scenarios like navigation history. Here's an example of managing a Breadcrumbs stack:
`typescript
export interface BreadcrumbsStackState {
breadcrumbs: Breadcrumb[];
}
export const initialBreadcrumbsStackState = {
breadcrumbs: []
}
`
`typescript
import { Injectable } from '@angular/core';
import { PublicRsmStackGenericClass } from 'rsm-signal-state-management';
@Injectable({
providedIn: 'root'
})
export class BreadcrumbsStoreService extends PublicRsmStackGenericClass {
constructor() {
super(initialBreadcrumbsStackState);
}
}
`
`typescript
breadcrumbsStoreService = inject(BreadcrumbsStoreService);
addNewRouteToBreadcrumbs(breadcrumb: Breadcrumb) {
this.breadcrumbsStoreService.pushToStack('breadcrumbs', breadcrumb);
}
goOneStepBackInBreadcrumbs() {
const currentRouteData: Signal = this.breadcrumbsStoreService.popFromStack('breadcrumbs');
}
`
You can push, pop, and observe items in the navigation stack with ease.
$3
Finally, the Actions State Manager allows you to create a message bus for dispatching and listening to actions within your application. Define your action types and use them to dispatch and listen for actions.
`typescript
import { Action } from 'rsm-signal-state-management';
export enum UserActionsEnum {
AddNewUser = '[User] Add',
RemoveUser = '[User] Remove'
}
export class AddNewUserActionType implements Action {
readonly type = UserActionsEnum.AddNewUser;
constructor(public payload: { user: User }) {}
}
export class RemoveUserActionType implements Action {
readonly type = UserActionsEnum.RemoveUser;
constructor(public payload: { userId: string }) {}
}
export type UserActionTypes = AddNewUserActionType | RemoveUserActionType;
`
`typescript
export interface UsersState {
users: User[];
}
export const initialUsersState: UsersState = {
users: []
};
`
`typescript
import { Injectable } from '@angular/core';
import { PublicActionsRsmGeneric } from 'rsm-signal-state-management';
@Injectable({
providedIn: 'root'
})
export class UserActionsService extends PublicActionsRsmGeneric {
constructor() {
super();
}
}
`
`typescript
import { Injectable } from "@angular/core";
import { PublicRsmEntityGeneric } from "rsm-signal-state-management";
@Injectable({
providedIn: 'root'
})
export class UsersStoreService extends PublicRsmActionsGeneric{
constructor() {
super(initialUsersState);
}
}
`
`typescript
userActionsService = inject(UserActionsService);
this.userActionsService.dispatchNewAction(new AddNewUserActionType(user));//user is an object with type User
`
`typescript
import { Injectable, effect,inject, Signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserEffectsService{
userStoreService = inject(UserStoreService);
userActionsService = inject(UserActionsService);
constructor() {
const action = this.userActionsService.actionListener();
this.createEffects(action);
}
private createEffects(action: Signal){
effect(() => {
switch(action().type) {
case UserActionsEnum.AddNewUser: {
this.http.post('url',action().payload).subscribe((user: User) =>{
this.userStoreService.addToArray('users', user);
})
break;
}
case RsmPrimitiveEnum.RemoveUser: {
this.http.delete('url',action().payload.userId).subscribe((user: User) =>{
this.userStoreService.removeFromArrayByProperty('users','id', user.id);
})
break;
}
}
});
}
}
`
You need to add the effect services into the app module's imports like below
`typescript
RsmEffectsModule.forRoot(UserEffectsService)
`
Or add it to the provider of standalone app like this:
`typescript
importProvidersFrom(RSMEffectsModule.forRoot(UserEffectsService))
`
API Documentation
$3
- updateState(statePropertyKey, data): Sets a state property in the store. you need to pass the statePropertyKey that shows which state property you wish to update, and the data which is the new value for that state property.
- setAllStates(allStates): Sets all the states at once and mainly uses for the initializing the states.
- select(statePropertyKey): Returns a specific property from the state as a Signal. Using statePropertyKey to specify which property you need to be selected.
- store: This property gives you the whole states value as a Signal.
$3
- getArraySize(statePropertyKey): This function takes a statePropertyKey as its input, which should be one of the keys from the state model provided to the generic class. Additionally, the specified key must correspond to an array type property. If this condition is not met, you will encounter a development-time error.
- addToArray(statePropertyKey, item, index): With this function, you can effortlessly append a new item or sub array to the start, end, or to an index of an array property within your state management, similar to how the push method works for arrays.
`typescript
interface User {
products: Product[],
history: History[]
}
this.userStoreService.addToArray('products', product, 'start'); // product must be an object with Product[] type
this.userStoreService.addToArray('products', product, 'end'); // product must be an object with Product[] type
this.userStoreService.addToArray('products', product, 2); // product must be an object with Product[] type
`
- updateArrayItemByIndex(statePropertyKey, index, item): This method updates an item at a specific index within an existing array.
- updateArrayItemByProperty(statePropertyKey, updatePropertyKey, updatePropertyValue, updateItem): This method updates an item from an existing array that has a property key with a certain value.
`typescript
interface UserProfile {
username: string;
firstName: string;
}
interface UserState {
users: UserProfile
}
const updateUser: UserProfile = {
username: 'test',
firstName: 'test name'
}
this.userStoreService.updateArrayItemByProperty('users','username','john', updateUser);
`
- removeFromArrayByIndex(statePropertyKey, index, deleteCount): This method removes some array items from a specific index.
- removeFromArrayByProperty(statePropertyKey, removePropertyKey, removePropertyValue): This method removes an item from an existing array that has a property key with a certain value.
`typescript
interface UserProfile {
username: string;
firstName: string;
lastName: string;
email: string;
age: number;
}
interface UserState {
users: UserProfile
}
this.userStoreService.removeFromArrayByProperty('users','username','john');
//or
this.userStoreService.removeFromArrayByProperty('users','age',12);
`
- getArrayItemByProperty(statePropertyKey, compareKey, compareValue): Retrieve the item with a certain key value in an existing array.
$3
You can utilize this class to manage various tasks such as handling modal systems, notification systems, and any logic that requires a queue or priority queue. Here are the useful functions of the Queue state manager and how to use them:
- getQueueSize(statePropertyKey): This method returns a signal representing the current size of the queue.
- addToQueue(statePropertyKey, item): Use this method to add a new item to the end of the queue.
- removeFromQueue(statePropertyKey): This method removes an item from the start of the queue and returns the removed item.
- addToPriorityQueue(statePropertyKey, priorityKey, priorityOrder, item): You can employ this method to add a new item to a priority queue. The priorityKey is a property key used to compare the priority of the new item with the priorities of existing items in the queue. The priorityOrder parameter specifies the order of priority, which can have two values: 'smaller-higher' (indicating that smaller values of priorityKey have higher priority) or 'bigger-higher' (indicating the opposite). This method ensures that the new item is added to the appropriate position in the priority queue based on its priority.
`typescript
this.userStoreService.addToPriorityQueue('userQueue','priority','smaller-higher',{username:'something',priority: 2, ...});
``