JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 23
  • Score
    100M100P100Q52002F

Package Exports

  • ngx-statewise
  • ngx-statewise/package.json

Readme

ngx-statewise

A lightweight and intuitive state management library for Angular.

Table of Contents

Description

ngx-statewise is a state management solution for Angular applications, offering a lighter and easier-to-use alternative to libraries like NgRx or NGXS, while maintaining a clear and predictable architecture for managing your application's state.

Features

  • 🔄 Flexible state management (supports Angular signals and regular properties)
  • 🧩 Modular and maintainable architecture
  • 📦 Predictable state updates
  • 🚀 Effects for handling asynchronous operations
  • 🔍 Easy to debug

Installation

npm install ngx-statewise --save

Key Concepts

1. States

States represent the current state of your application or a specific feature. They can be defined using Angular signals or regular properties.

@Injectable({
  providedIn: 'root',
})
export class AuthStates {
  public user = signal<User | null>(null);
  public accessToken: string | null = null;  // Regular property
  public isLoggedIn = signal(false);
  public isLoading = signal(false);
  public asError = signal(false);
}

2. Actions

Actions are events that trigger state changes. They can be defined individually or as a group. You can include any events you want in an action group - you're not limited to the request-success-failure pattern.

Action Group

When using defineActionsGroupe, action types are automatically created by combining the source and event name. For example, with a source of 'LOGIN', events like 'request' will become 'LOGIN_REQUEST'.

export const loginActions = defineActionsGroupe({
  source: 'LOGIN',
  events: {
    request: payload<LoginSubmit>(),  // Becomes LOGIN_REQUEST
    success: payload<LoginResponses>(),  // Becomes LOGIN_SUCCESS
    failure: emptyPayload,  // Becomes LOGIN_FAILURE
    cancel: emptyPayload,  // Becomes LOGIN_CANCEL
    retry: payload<number>(),  // Becomes LOGIN_RETRY
  },
});

Single Action

For single actions defined with defineSingleAction, the type will always be suffixed with '_ACTION'. For example, 'LOGOUT' becomes 'LOGOUT_ACTION'.

export const logoutAction = defineSingleAction('LOGOUT', emptyPayload);  // Becomes LOGOUT_ACTION
export const selectItemAction = defineSingleAction('SELECT_ITEM', payload<number>());  // Becomes SELECT_ITEM_ACTION

3. Updators

Updators are responsible for updating the state in response to actions. The action type key used in the updator must exactly match the type generated by the action definition. Updators should focus solely on modifying state data - any side effects or operations not directly related to state updates should be placed in Effects.

@Injectable({
  providedIn: 'root',
})
export class AuthUpdator implements IUpdator<AuthStates> {
  public readonly state = inject(AuthStates);

  public readonly updators: UpdatorRegistry<AuthStates> = {
    LOGIN_REQUEST: (state) => {
      // Only modify state data, no side effects here
      state.isLoading.set(true);
      state.asError.set(false);
    },
    LOGIN_SUCCESS: (state, payload: LoginResponses) => {
      state.user.set({
        userId: payload.userId,
        userName: payload.userName,
        email: payload.email,
      });
      state.isLoggedIn.set(true);
      state.isLoading.set(false);
    },
    LOGIN_FAILURE: (state) => {
      state.user.set(null);
      state.isLoggedIn.set(false);
      state.asError.set(true);
      state.isLoading.set(false);
    },
    LOGOUT_ACTION: (state) => {
      state.user.set(null);
      state.isLoggedIn.set(false);
      state.asError.set(false);
      state.isLoading.set(false);
    },
  };
}

4. Effects

Effects handle asynchronous operations like API calls. They are created with the createEffect utility function and are tied to specific actions. Effects can return other actions to trigger updators or other effects, creating a chain of operations. Be careful not to return the input action to avoid infinite loops.

@Injectable({
  providedIn: 'root',
})
export class AuthEffects {
  private readonly authRepository = inject(AuthRepositoryService);
  private readonly authTokenService = inject(AuthTokenService);
  private readonly router = inject(Router);
  
  public readonly loginEffect = createEffect(
    loginActions.request,  // Triggered by the request action
    (payload) => {
      return this.authRepository.login(payload).pipe(
        map((res) => {
          // On success, return the success action with payload
          this.router.navigate(['/']);
          this.authTokenService.setAccessToken(res.body?.accessToken!);
          return loginActions.success(res.body!);  // Success action
        }),
        catchError(() => of(loginActions.failure()))  // Failure action on error
        // Don't return loginActions.request() here or you'll create an infinite loop!
      );
    }
  );
  
  public readonly logoutEffect = createEffect(
    logoutAction.action,
    () => {
      this.router.navigate(['/']);
      return EMPTY;  // No additional action needed
    }
  );
}

Registering Effects

Effect classes must be registered using provideEffects in your app config. Without this registration, the effects won't be initialized and nothing will happen when actions are dispatched.

export const appConfig: ApplicationConfig = {
  providers: [
    provideEffects([
      AuthEffects,
      UserEffects,
      // Add all your effect classes here
    ]),
    // other providers
  ]
};

This approach ensures all your effects are properly registered and instantiated when your application starts, allowing them to listen for actions and perform side effects.

5. Managers

Managers expose the public APIs for interacting with the state. They expose state data (signals or regular properties) and provide methods that trigger actions, creating a clean interface for components to use.

@Injectable({
  providedIn: 'root',
})
export class AuthManager implements IAuthManager {
  private readonly authStates = inject(AuthStates);
  private readonly authEffects = inject(AuthEffects);
  private readonly authUpdator = inject(AuthUpdator);
  
  // Expose states for components to access
  public readonly user = this.authStates.user;
  public readonly isLoggedIn = this.authStates.isLoggedIn;
  public readonly isLoading = this.authStates.isLoading;
  public readonly asError = this.authStates.asError;
  
  // Define methods that trigger actions
  public login(credential: LoginSubmit): void {
    dispatch(
      loginActions.request(credential),
      this.authUpdator
    );
  }
  
  public logout() {
    dispatch(
      logoutAction.action(),
      this.authUpdator
    );
  }
}

Usage Example

Defining States

@Injectable({
  providedIn: 'root',
})
export class TodoStates {
  public todos = signal<Todo[]>([]);
  public isLoading = signal(false);
  public error = signal<string | null>(null);
  public selectedTodoId: number | null = null;  // Regular property
}

Defining Actions

export const todoActions = defineActionsGroupe({
  source: 'TODO',
  events: {
    load: emptyPayload,  // Becomes TODO_LOAD
    loadSuccess: payload<Todo[]>(),  // Becomes TODO_LOAD_SUCCESS
    loadFailure: payload<string>(),  // Becomes TODO_LOAD_FAILURE
    add: payload<Todo>(),  // Becomes TODO_ADD
    remove: payload<number>(),  // Becomes TODO_REMOVE
    select: payload<number>(),  // Becomes TODO_SELECT
  },
});

Defining Updators

@Injectable({
  providedIn: 'root',
})
export class TodoUpdator implements IUpdator<TodoStates> {
  public readonly state = inject(TodoStates);

  public readonly updators: UpdatorRegistry<TodoStates> = {
    TODO_LOAD: (state) => {
      state.isLoading.set(true);
      state.error.set(null);
    },
    TODO_LOAD_SUCCESS: (state, payload: Todo[]) => {
      state.todos.set(payload);
      state.isLoading.set(false);
    },
    TODO_LOAD_FAILURE: (state, payload: string) => {
      state.error.set(payload);
      state.isLoading.set(false);
    },
    TODO_ADD: (state, payload: Todo) => {
      const currentTodos = state.todos();
      state.todos.set([...currentTodos, payload]);
    },
    TODO_REMOVE: (state, payload: number) => {
      const currentTodos = state.todos();
      state.todos.set(currentTodos.filter(todo => todo.id !== payload));
    },
    TODO_SELECT: (state, payload: number) => {
      // Example of updating a regular property
      state.selectedTodoId = payload;
    },
  };
}

Creating Effects

@Injectable({
  providedIn: 'root',
})
export class TodoEffects {
  private readonly todoService = inject(TodoService);
  
  public readonly loadTodosEffect = createEffect(
    todoActions.load,
    () => {
      return this.todoService.getTodos().pipe(
        map(todos => todoActions.loadSuccess(todos)),
        catchError(error => of(todoActions.loadFailure(error.message)))
      );
    }
  );
  
  public readonly addTodoEffect = createEffect(
    todoActions.add,
    (todo) => {
      return this.todoService.addTodo(todo).pipe(
        map(() => todoActions.load()),  // Chain to load action after adding
        catchError(error => of(todoActions.loadFailure(error.message)))
      );
    }
  );
}

Usage in Components

@Component({
  selector: 'app-todo-list',
  template: `
    @if (isLoading()) {
      <div>Loading...</div>
    }
    @if (error()) {
      <div>{{ error() }}</div>
    }
    <ul>
      @for (todo of todos(); track todo.id) {
        <li [class.selected]="todo.id === todoManager.selectedTodoId"
            (click)="selectTodo(todo.id)">
          {{ todo.title }}
          <button (click)="removeTodo(todo.id)">Delete</button>
        </li>
      }
    </ul>
    <button (click)="loadTodos()">Refresh</button>
  `,
})
export class TodoListComponent {
  private todoManager = inject(TodoManager);
 
  public todos = this.todoManager.todos;
  public isLoading = this.todoManager.isLoading;
  public error = this.todoManager.error;
 
  loadTodos(): void {
    this.todoManager.loadTodos();
  }
 
  removeTodo(id: number): void {
    this.todoManager.removeTodo(id);
  }
 
  selectTodo(id: number): void {
    this.todoManager.selectTodo(id);
  }
}

Benefits

  • Simplicity: Simpler and more intuitive API than NgRx or NGXS
  • Performance: Optional use of Angular signals for optimized change detection
  • Modularity: Clear organization of states, actions, and effects
  • Testability: Architecture that facilitates unit testing
  • Flexibility: Works with both signals and regular properties, allowing for gradual adoption

When to Use ngx-statewise

  • Medium to large Angular applications
  • Applications with complex state management needs
  • Teams looking for a balance between structure and simplicity
  • Projects that want to leverage Angular signals but need more structure

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests on GitHub.

License

GPL v3