Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/nigrosimone/ng-simple-state

Simple state management in Angular with only Services and RxJS or Signal.
https://github.com/nigrosimone/ng-simple-state

angular angular2 rxjs state state-management

Last synced: 28 days ago
JSON representation

Simple state management in Angular with only Services and RxJS or Signal.

Awesome Lists containing this project

README

        

# NgSimpleState [![Build Status](https://app.travis-ci.com/nigrosimone/ng-simple-state.svg?branch=main)](https://app.travis-ci.com/nigrosimone/ng-simple-state) [![Coverage Status](https://coveralls.io/repos/github/nigrosimone/ng-simple-state/badge.svg?branch=main)](https://coveralls.io/github/nigrosimone/ng-simple-state?branch=main) [![NPM version](https://img.shields.io/npm/v/ng-simple-state.svg)](https://www.npmjs.com/package/ng-simple-state) [![Maintainability](https://api.codeclimate.com/v1/badges/1bfc363a95053ecc3429/maintainability)](https://codeclimate.com/github/nigrosimone/ng-simple-state/maintainability)

Simple state management in Angular with only Services and RxJS or Signal.

## Description

Sharing state between components as simple as possible and leverage the good parts of component state and Angular's dependency injection system.

See the demos:
- [Counter](https://stackblitz.com/edit/demo-ng-simple-state?file=src%2Fapp%2Fapp.component.ts)
- [Tour of heroes](https://stackblitz.com/edit/ng-simple-state-tour-of-heroes?file=src%2Fapp%2Fhero.service.ts)
- [To Do List](https://stackblitz.com/edit/ng-simple-state-todo?file=src%2Fapp%2Fapp.component.ts)

## Get Started

### Step 1: install `ng-simple-state`

```bash
npm i ng-simple-state
```

### Step 2: Import `provideNgSimpleState` into your providers

`provideNgSimpleState` has some global optional config defined by `NgSimpleStateConfig` interface:

| Option | Description | Default |
| -------------------- | ----------------------------------------------------------------------------------------------- | ---------- |
| *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
| *enableLocalStorage* | if `true` latest state of store is saved in local storage and reloaded on store initialization. | `false` |
| *persistentStorage* | Set the persistent storage `local` or `session`. | `local` |
| *comparator* | A function used to compare the previous and current state for equality. | `a === b` |

_Side note: each store can be override the global configuration implementing `storeConfig()` method (see "Override global config")._

```ts
import { isDevMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { provideNgSimpleState } from 'ng-simple-state';

bootstrapApplication(AppComponent, {
providers: [
provideNgSimpleState({
enableDevTool: isDevMode(),
enableLocalStorage: true,
persistentStorage: 'local'
})
]
});
```

### Step 3: Chose your store

There are two type of store `NgSimpleStateBaseRxjsStore` based on RxJS `BehaviorSubject` and `NgSimpleStateBaseSignalStore` based on Angular `Signal`:

- [RxJS Store](#rxjs-store)
- [Signal Store](#signal-store)

## RxJS Store

This is an example for a counter store in a `src/app/counter-store.ts` file.
Obviously, you can create every store you want with every complexity you need.

1) Define your state interface, eg.:

```ts
export interface CounterState {
count: number;
}
```

2) Define your store service by extending `NgSimpleStateBaseRxjsStore`, eg.:

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';

export interface CounterState {
count: number;
}

@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {

}
```

3) Implement `initialState()` and `storeConfig()` methods and provide the initial state of the store, eg.:

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';

export interface CounterState {
count: number;
}

@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}

initialState(): CounterState {
return {
count: 0
};
}

}
```

4) Implement one or more selectors of the partial state you want, in this example `selectCount()` eg.:

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
import { Observable } from 'rxjs';

export interface CounterState {
count: number;
}

@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}

initialState(): CounterState {
return {
count: 0
};
}

selectCount(): Observable {
return this.selectState(state => state.count);
}
}
```

5) Implement one or more actions for change the store state, in this example `increment()` and `decrement()` eg.:

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore, NgSimpleStateStoreConfig } from 'ng-simple-state';
import { Observable } from 'rxjs';

export interface CounterState {
count: number;
}

@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}

initialState(): CounterState {
return {
count: 0
};
}

selectCount(): Observable {
return this.selectState(state => state.count);
}

increment(increment: number = 1): void {
this.setState(state => ({ count: state.count + increment }));
}

decrement(decrement: number = 1): void {
this.setState(state => ({ count: state.count - decrement }));
}
}
```

#### Step 3: Inject your store into the providers, eg.:

```ts
import { Component } from '@angular/core';
import { CounterStore } from './counter-store';

@Component({
selector: 'app-root',
imports: [CounterStore]
})
export class AppComponent {

}
```

#### Step 4: Use your store into the components, eg.:

```ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { CounterStore } from './counter-store';

@Component({
selector: 'app-root',
imports: [CounterStore],
template: `

Counter: {{ counter$ | async }}


Decrement
Reset
Increment
`,
})
export class AppComponent {
public counter$: Observable;

constructor(public counterStore: CounterStore) {
this.counter$ = this.counterStore.selectCount();
}
}
```

#### That's all!

![alt text](https://github.com/nigrosimone/ng-simple-state/blob/main/projects/ng-simple-state-demo/src/assets/dev-tool.gif?raw=true)

### Manage component state without service

If you want manage just a component state without make a new service, your component can extend directly `NgSimpleStateBaseRxjsStore`:

```ts
import { Component } from '@angular/core';
import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
import { Observable } from 'rxjs';

export interface CounterState {
count: number;
}

@Component({
selector: 'ng-counter',
template: `
{{counter$ | async}}
+
-
`
})
export class CounterComponent extends NgSimpleStateBaseRxjsStore {

public counter$: Observable = this.selectState(state => state.count);

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterComponent'
};
}

initialState(): CounterState {
return {
count: 0
};
}

increment(): void {
this.setState(state => ({ count: state.count + 1 }));
}

decrement(): void {
this.setState(state => ({ count: state.count - 1 }));
}
}
```

### Override global config

If you need to override the global configuration provided by `provideNgSimpleState()` you can implement `storeConfig()` and return a specific configuration for the single store, eg.:

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateStoreConfig } from 'ng-simple-state';

@Injectable()
export class CounterStore extends NgSimpleStateBaseRxjsStore {

override storeConfig(): NgSimpleStateStoreConfig {
return {
enableLocalStorage: true, // enable local storage for this store
persistentStorage: 'session', // persistentStorage can be 'session' or 'local' (default is localStorage)
storeName: 'CounterStore2', // For default the store name is the class name, you can set a specific name for this store (must be be unique)
}
}
}
```

The options are defined by `NgSimpleStateStoreConfig` interface:

| Option | Description | Default |
| -------------------- | ----------------------------------------------------------------------------------------------- | ---------- |
| *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
| *enableLocalStorage* | if `true` latest state of store is saved in local storage and reloaded on store initialization. | `false` |
| *storeName* | The name used into `Redux DevTools` and local storage key. | Class name |
| *persistentStorage* | Set the persistent storage `local` or `session` | `local` |
| *comparator* | A function used to compare the previous and current state for equality. | `a === b` |

### Testing

`ng-simple-state` is simple to test. Eg.:

```ts
import { TestBed } from '@angular/core/testing';
import { provideNgSimpleState } from 'ng-simple-state';
import { CounterStore } from './counter-store';

describe('CounterStore', () => {

let counterStore: CounterStore;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideNgSimpleState({
enableDevTool: false,
enableLocalStorage: false
}),
CounterStore
]
});

counterStore = TestBed.inject(CounterStore);
});

it('initialState', () => {
expect(counterStore.getCurrentState()).toEqual({ count: 0 });
});

it('increment', () => {
counterStore.increment();
expect(counterStore.getCurrentState()).toEqual({ count: 1 });
});

it('decrement', () => {
counterStore.decrement();
expect(counterStore.getCurrentState()).toEqual({ count: -1 });
});

it('selectCount', (done) => {
counterStore.selectCount().subscribe(value => {
expect(value).toBe(0);
done();
});
});

});
```

### Example: array store

This is an example for a todo list store in a `src/app/todo-store.ts` file.

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseRxjsStore } from 'ng-simple-state';
import { Observable } from 'rxjs';

export interface Todo {
id: number;
name: string;
completed: boolean;
}

export type TodoState = Array;

@Injectable()
export class TodoStore extends NgSimpleStateBaseRxjsStore {

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'TodoStore'
};
}

initialState(): TodoState {
return [];
}

add(todo: Omit): void {
this.setState(state => [...state, {...todo, id: Date.now()}]);
}

delete(id: number): void {
this.setState(state => state.filter(item => item.id !== id) );
}

setComplete(id: number, completed: boolean = true): void {
this.setState(state => state.map(item => item.id === id ? {...item, completed} : item) );
}
}
```

usage:

```ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Todo, TodoStore } from './todo-store';

@Component({
selector: 'app-root',
template: `
Add todo




  1. {{ todo.name }}
    Mark as {{ todo.completed ? 'Not completed' : 'Completed' }}
    Delete


`,
providers: [TodoStore]
})
export class AppComponent {
public todoList$: Observable;

constructor(public todoStore: TodoStore) {
this.todoList$ = this.todoStore.selectState();
}
}
```

### NgSimpleStateBaseRxjsStore API

```ts
@Injectable()
@Directive()
export abstract class NgSimpleStateBaseRxjsStore> implements OnDestroy {

/**
* Return the observable of the state
* @returns Observable of the state
*/
public get state(): BehaviorSubject;

/**
* When you override this method, you have to call the `super.ngOnDestroy()` method in your `ngOnDestroy()` method.
*/
ngOnDestroy(): void;

/**
* Reset store to first loaded store state:
* - the last saved state, if `enableLocalStorage` config is `true`
* - otherwise the initial state provided from `initialState()` method.
*/
resetState(): boolean;

/**
* Restart the store to initial state provided from `initialState()` method
*/
restartState(): boolean;

/**
* Override this method for set a specific config for the store
* @returns NgSimpleStateStoreConfig
*/
storeConfig(): NgSimpleStateStoreConfig;

/**
* Set into the store the initial state
* @returns The state object
*/
initialState(): S;

/**
* Select a store state
* @param selectFn State selector (if not provided return full state)
* @param comparator A function used to compare the previous and current state for equality. Defaults to a `===` check.
* @returns Observable of the selected state
*/
selectState(selectFn?: (state: Readonly) => K, comparator?: (previous: K, current: K) => boolean): Observable;

/**
* Return the current store state (snapshot)
* @returns The current state
*/
getCurrentState(): Readonly;

/**
* Return the first loaded store state:
* the last saved state, if `enableLocalStorage` config is `true`;
* otherwise the initial state provided from `initialState()` method.
* @returns The first state
*/
getFirstState(): Readonly | null;

/**
* Set a new state
* @param selectFn State reducer
* @param actionName The action label into Redux DevTools (default is parent function name)
* @returns True if the state is changed
*/
setState(stateFn: (currentState: Readonly) => Partial, actionName?: string): boolean;
}
```
## Signal Store

This is an example for a counter store in a `src/app/counter-store.ts` file.
Obviously, you can create every store you want with every complexity you need.

1) Define your state interface, eg.:

```ts
export interface CounterState {
count: number;
}
```

2) Define your store service by extending `NgSimpleStateBaseSignalStore`, eg.:

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';

export interface CounterState {
count: number;
}

@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {

}
```

3) Implement `initialState()` and `storeConfig()` methods and provide the initial state of the store, eg.:

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';

export interface CounterState {
count: number;
}

@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}

initialState(): CounterState {
return {
count: 0
};
}

}
```

4) Implement one or more selectors of the partial state you want, in this example `selectCount()` eg.:

```ts
import { Injectable, Signal } from '@angular/core';
import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';

export interface CounterState {
count: number;
}

@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}

initialState(): CounterState {
return {
count: 0
};
}

selectCount(): Signal {
return this.selectState(state => state.count);
}
}
```

5) Implement one or more actions for change the store state, in this example `increment()` and `decrement()` eg.:

```ts
import { Injectable, Signal } from '@angular/core';
import { NgSimpleStateBaseSignalStore, NgSimpleStateStoreConfig } from 'ng-simple-state';

export interface CounterState {
count: number;
}

@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterStore'
};
}

initialState(): CounterState {
return {
count: 0
};
}

selectCount(): Signal {
return this.selectState(state => state.count);
}

increment(increment: number = 1): void {
this.setState(state => ({ count: state.count + increment }));
}

decrement(decrement: number = 1): void {
this.setState(state => ({ count: state.count - decrement }));
}
}
```

#### Step 3: Inject your store into the providers, eg.:

```ts
import { Component } from '@angular/core';
import { CounterStore } from './counter-store';

@Component({
selector: 'app-root',
imports: [CounterStore]
})
export class AppComponent {

}
```

#### Step 4: Use your store into the components, eg.:

```ts
import { Component, Signal } from '@angular/core';
import { CounterStore } from './counter-store';

@Component({
selector: 'app-root',
template: `

Counter: {{ counterSig() }}


Decrement
Reset
Increment
`,
})
export class AppComponent {
public counterSig: Signal;

constructor(public counterStore: CounterStore) {
this.counterSig = this.counterStore.selectCount();
}
}
```

#### That's all!

![alt text](https://github.com/nigrosimone/ng-simple-state/blob/main/projects/ng-simple-state-demo/src/assets/dev-tool.gif?raw=true)

### Manage component state without service

If you want manage just a component state without make a new service, your component can extend directly `NgSimpleStateBaseSignalStore`:

```ts
import { Component, Signal } from '@angular/core';
import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';

export interface CounterState {
count: number;
}

@Component({
selector: 'ng-counter',
template: `
{{counterSig()}}
+
-
`
})
export class CounterComponent extends NgSimpleStateBaseSignalStore {

public counterSig: Signal = this.selectState(state => state.count);

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'CounterComponent'
};
}

initialState(): CounterState {
return {
count: 0
};
}

increment(): void {
this.setState(state => ({ count: state.count + 1 }));
}

decrement(): void {
this.setState(state => ({ count: state.count - 1 }));
}
}
```

### Override global config

If you need to override the global configuration provided by `provideNgSimpleState()` you can implement `storeConfig()` and return a specific configuration for the single store, eg.:

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateStoreConfig } from 'ng-simple-state';

@Injectable()
export class CounterStore extends NgSimpleStateBaseSignalStore {

override storeConfig(): NgSimpleStateStoreConfig {
return {
enableLocalStorage: true, // enable local storage for this store
persistentStorage: 'session', // persistentStorage can be 'session' or 'local' (default is localStorage)
storeName: 'CounterStore2', // For default the store name is the class name, you can set a specific name for this store (must be be unique)
}
}
}
```

The options are defined by `NgSimpleStateStoreConfig` interface:

| Option | Description | Default |
| -------------------- | ----------------------------------------------------------------------------------------------- | ---------- |
| *enableDevTool* | if `true` enable `Redux DevTools` browser extension for inspect the state of the store. | `false` |
| *enableLocalStorage* | if `true` latest state of store is saved in local storage and reloaded on store initialization. | `false` |
| *storeName* | The name used into `Redux DevTools` and local storage key. | Class name |
| *persistentStorage* | Set the persistent storage `local` or `session` | `local` |
| *comparator* | A function used to compare the previous and current state for equality. | `a === b` |

### Testing

`ng-simple-state` is simple to test. Eg.:

```ts
import { TestBed } from '@angular/core/testing';
import { provideNgSimpleState } from 'ng-simple-state';
import { CounterStore } from './counter-store';

describe('CounterStore', () => {

let counterStore: CounterStore;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideNgSimpleState({
enableDevTool: false,
enableLocalStorage: false
}),
CounterStore
]
});

counterStore = TestBed.inject(CounterStore);
});

it('initialState', () => {
expect(counterStore.getCurrentState()).toEqual({ count: 0 });
});

it('increment', () => {
counterStore.increment();
expect(counterStore.getCurrentState()).toEqual({ count: 1 });
});

it('decrement', () => {
counterStore.decrement();
expect(counterStore.getCurrentState()).toEqual({ count: -1 });
});

it('selectCount', () => {
const valueSig = counterStore.selectCount();
expect(valueSig()).toBe(0);
});

});
```

### Example: array store

This is an example for a todo list store in a `src/app/todo-store.ts` file.

```ts
import { Injectable } from '@angular/core';
import { NgSimpleStateBaseSignalStore } from 'ng-simple-state';

export interface Todo {
id: number;
name: string;
completed: boolean;
}

export type TodoState = Array;

@Injectable()
export class TodoStore extends NgSimpleStateBaseSignalStore {

storeConfig(): NgSimpleStateStoreConfig {
return {
storeName: 'TodoStore'
};
}

initialState(): TodoState {
return [];
}

add(todo: Omit): void {
this.setState(state => [...state, {...todo, id: Date.now()}]);
}

delete(id: number): void {
this.setState(state => state.filter(item => item.id !== id) );
}

setComplete(id: number, completed: boolean = true): void {
this.setState(state => state.map(item => item.id === id ? {...item, completed} : item) );
}
}
```

usage:

```ts
import { Component, Signal } from '@angular/core';
import { Todo, TodoStore } from './todo-store';

@Component({
selector: 'app-root',
template: `
Add todo




  1. {{ todo.name }}
    Mark as {{ todo.completed ? 'Not completed' : 'Completed' }}
    Delete


`,
providers: [TodoStore]
})
export class AppComponent {
public todoListSig: Signal;

constructor(public todoStore: TodoStore) {
this.todoListSig = this.todoStore.selectState();
}
}
```

### NgSimpleStateBaseSignalStore API

```ts
@Injectable()
@Directive()
export abstract class NgSimpleStateBaseSignalStore> implements OnDestroy {

/**
* Return the Signal of the state
* @returns Signal of the state
*/
public get state(): Signal;

/**
* When you override this method, you have to call the `super.ngOnDestroy()` method in your `ngOnDestroy()` method.
*/
ngOnDestroy(): void;

/**
* Reset store to first loaded store state:
* - the last saved state, if `enableLocalStorage` config is `true`
* - otherwise the initial state provided from `initialState()` method.
*/
resetState(): boolean;

/**
* Restart the store to initial state provided from `initialState()` method
*/
restartState(): boolean;

/**
* Override this method for set a specific config for the store
* @returns NgSimpleStateStoreConfig
*/
storeConfig(): NgSimpleStateStoreConfig;

/**
* Set into the store the initial state
* @returns The state object
*/
initialState(): S;

/**
* Select a store state
* @param selectFn State selector (if not provided return full state)
* @param comparator A function used to compare the previous and current state for equality. Defaults to a `===` check.
* @returns Signal of the selected state
*/
selectState(selectFn?: (state: Readonly) => K, comparator?: (previous: K, current: K) => boolean): Signal;

/**
* Return the current store state (snapshot)
* @returns The current state
*/
getCurrentState(): Readonly;

/**
* Return the first loaded store state:
* the last saved state, if `enableLocalStorage` config is `true`;
* otherwise the initial state provided from `initialState()` method.
* @returns The first state
*/
getFirstState(): Readonly | null;

/**
* Set a new state
* @param selectFn State reducer
* @param actionName The action label into Redux DevTools (default is parent function name)
* @returns True if the state is changed
*/
setState(stateFn: (currentState: Readonly) => Partial, actionName?: string): boolean;
}
```

## Alternatives

Aren't you satisfied? there are some valid alternatives:

- [@tinystate](https://www.npmjs.com/package/@tinystate/core)
- [@ngxs](https://www.npmjs.com/package/@ngxs/store)
## Support

This is an open-source project. Star this [repository](https://github.com/nigrosimone/ng-simple-state), if you like it, or even [donate](https://www.paypal.com/paypalme/snwp). Thank you so much!

## My other libraries

I have published some other Angular libraries, take a look:

- [NgHttpCaching: Cache for HTTP requests in Angular application](https://www.npmjs.com/package/ng-http-caching)
- [NgGenericPipe: Generic pipe for Angular application for use a component method into component template.](https://www.npmjs.com/package/ng-generic-pipe)
- [NgLet: Structural directive for sharing data as local variable into html component template](https://www.npmjs.com/package/ng-let)
- [NgForTrackByProperty: Angular global trackBy property directive with strict type checking](https://www.npmjs.com/package/ng-for-track-by-property)