npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@polystate/angular

v0.2.0

Published

Angular 17+ services for Polystate state management

Downloads

96

Readme

@polystate/angular

Angular 17+ services for Polystate state management.

Features

  • Angular 17+ Signals: Native Signal support
  • RxJS Compatible: Observable integration with async pipe
  • Lightweight: < 1kb gzipped
  • Type-Safe: Full TypeScript support
  • Dependency Injection: Standard Angular DI patterns

Installation

npm install @angular/core @angular/common rxjs
npm install @polystate/core @polystate/angular

Quick Start

import { Injectable } from '@angular/core';
import { createStore } from '@polystate/core';
import { PolystateService } from '@polystate/angular';

// Define state type
interface CounterState {
  count: number;
}

// Create service
@Injectable({ providedIn: 'root' })
export class CounterService extends PolystateService<CounterState> {
  // Create store
  private store = createStore<CounterState>(
    { count: 0 },
    {
      increment: (state) => ({ ...state, count: state.count + 1 }),
      decrement: (state) => ({ ...state, count: state.count - 1 }),
    }
  );

  // Create signals
  count = this.select((state) => state.count);
  count$ = this.select$((state) => state.count);

  increment() {
    this.dispatch('increment');
  }

  decrement() {
    this.dispatch('decrement');
  }
}

Use in component:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-counter',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div>
      <!-- Using Signal -->
      <p>Count (Signal): {{ counter.count() }}</p>

      <!-- Using Observable with async pipe -->
      <p>Count (Observable): {{ counter.count$ | async }}</p>

      <button (click)="counter.increment()">+</button>
      <button (click)="counter.decrement()">-</button>
    </div>
  `,
})
export class CounterComponent {
  constructor(public counter: CounterService) {}
}

API

PolystateService

Abstract base class for Polystate services.

select

Returns Angular Signal with selected state slice.

export class TodoService extends PolystateService<TodoState> {
  private store = createStore({ todos: [] }, {...});

  // Returns Signal<Todo[]>
  todos = this.select((state) => state.todos);

  // Usage in template
  // {{ service.todos() }}
}

Returns: () => S (Angular Signal)

select$

Returns RxJS Observable with selected state slice.

export class TodoService extends PolystateService<TodoState> {
  private store = createStore({ todos: [] }, {...});

  // Returns Observable<Todo[]>
  todos$ = this.select$((state) => state.todos);

  // Usage in template
  // {{ service.todos$ | async }}
}

Returns: Observable<S>

dispatch

Dispatch an action.

service.dispatch('addTodo', 'Learn Angular');
service.dispatch('toggleTodo', todoId);

getState

Get current state snapshot.

const state = service.getState();
const todos = service.getState((state) => state.todos);

Complete Example

// todo.service.ts
import { Injectable } from '@angular/core';
import { createStore } from '@polystate/core';
import { PolystateService } from '@polystate/angular';

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

export interface TodoState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
}

@Injectable({ providedIn: 'root' })
export class TodoService extends PolystateService<TodoState> {
  private todoStore = createStore<TodoState>(
    { todos: [], filter: 'all' },
    {
      addTodo: (state, text: string) => ({
        ...state,
        todos: [...state.todos, { id: Date.now(), text, completed: false }],
      }),
      removeTodo: (state, id: number) => ({
        ...state,
        todos: state.todos.filter((todo) => todo.id !== id),
      }),
      toggleTodo: (state, id: number) => ({
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === id ? { ...todo, completed: !todo.completed } : todo
        ),
      }),
      setFilter: (state, filter) => ({ ...state, filter }),
    }
  );

  // Signals
  todos = this.select((state) => state.todos);
  filter = this.select((state) => state.filter);

  // Observables
  todos$ = this.select$((state) => state.todos);
  filter$ = this.select$((state) => state.filter);

  // Computed signal (Angular 18+)
  filteredTodos = computed(() => {
    const todos = this.todos();
    const filter = this.filter();

    return todos.filter((todo) => {
      if (filter === 'completed') return todo.completed;
      if (filter === 'active') return !todo.completed;
      return true;
    });
  });

  addTodo(text: string) {
    this.dispatch('addTodo', text);
  }

  removeTodo(id: number) {
    this.dispatch('removeTodo', id);
  }

  toggleTodo(id: number) {
    this.dispatch('toggleTodo', id);
  }

  setFilter(filter: 'all' | 'active' | 'completed') {
    this.dispatch('setFilter', filter);
  }
}

// todo.component.ts
import { Component, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TodoService } from './todo.service';

@Component({
  selector: 'app-todos',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <div class="todos">
      <h2>Todo List</h2>

      <div class="input-group">
        <input
          #input
          type="text"
          placeholder="Add a todo..."
          (keyup.enter)="addTodo(input.value); input.value = ''"
        />
        <button (click)="addTodo(input.value); input.value = ''">Add</button>
      </div>

      <div class="filters">
        <button
          *ngFor="let f of ['all', 'active', 'completed']"
          [disabled]="todos.filter() === f"
          (click)="todos.setFilter(f)"
        >
          {{ f | titlecase }}
        </button>
      </div>

      <ul>
        <li *ngFor="let todo of todos.filteredTodos()">
          <input type="checkbox" [checked]="todo.completed" (change)="todos.toggleTodo(todo.id)" />
          <span [class.completed]="todo.completed">
            {{ todo.text }}
          </span>
          <button (click)="todos.removeTodo(todo.id)">Delete</button>
        </li>
      </ul>
    </div>
  `,
  styles: [
    `
      .todos {
        max-width: 500px;
        margin: 0 auto;
      }
    `,
  ],
})
export class TodosComponent {
  constructor(public todos: TodoService) {}

  addTodo(text: string) {
    if (text.trim()) {
      this.todos.addTodo(text.trim());
    }
  }
}

Using Observable Pattern

Subscribe to observables in components:

@Component({
  template: `
    <div *ngFor="let todo of todos$ | async">
      {{ todo.text }}
    </div>
  `,
})
export class TodoListComponent {
  todos$ = this.todoService.todos$;

  constructor(private todoService: TodoService) {}
}

Using Signal Pattern

Use signals directly for better performance:

@Component({
  template: `
    <div *ngFor="let todo of todos()">
      {{ todo.text }}
    </div>
  `,
})
export class TodoListComponent {
  todos = this.todoService.todos;

  constructor(private todoService: TodoService) {}
}

Computed Signals

Combine signals for derived state:

@Injectable({ providedIn: 'root' })
export class TodoService extends PolystateService<TodoState> {
  //...

  // Computed signal (Angular 18+)
  activeCount = computed(() => {
    const todos = this.todos();
    return todos.filter((t) => !t.completed).length;
  });

  completedCount = computed(() => {
    const todos = this.todos();
    return todos.filter((t) => t.completed).length;
  });
}

Testing

import { TestBed } from '@angular/core/testing';
import { TodoService } from './todo.service';

describe('TodoService', () => {
  let service: TodoService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [TodoService],
    });
    service = TestBed.inject(TodoService);
  });

  it('should add a todo', async () => {
    await service.addTodo('Learn Polystate');

    const todos = service.todos();
    expect(todos).toHaveLength(1);
    expect(todos[0].text).toBe('Learn Polystate');
  });

  it('should toggle todo completion', async () => {
    await service.addTodo('Test');
    const todo = service.todos()[0];

    await service.toggleTodo(todo.id);

    expect(service.todos()[0].completed).toBe(true);
  });

  it('should filter todos', async () => {
    await service.addTodo('Active todo');
    await service.addTodo('Completed todo');

    const todos = service.todos();
    await service.toggleTodo(todos[1].id);

    await service.setFilter('active');
    expect(service.todos()[0].text).toBe('Active todo');

    await service.setFilter('completed');
    expect(service.todos()[0].text).toBe('Completed todo');
  });
});

SSR Support (Angular Universal)

Polystate works with Angular Universal out of the box:

import { renderModule } from '@angular/platform-server';
import { AppServerModule } from './app/app.server.module';

export default function render(req: Request): Promise<string> {
  const { origin } = new URL(req.url);

  // Services are created per-request
  return renderModule(AppServerModule, {
    document: getDocument(),
    url: `${origin}${req.url}`,
    providers: [
      // Provide any SSR-specific dependencies
    ],
  });
}

Middleware Support

Use middleware in Angular services:

import { persistMiddleware, loggerMiddleware } from '@polystate/core';

@Injectable({ providedIn: 'root' })
export class TodoService extends PolystateService<TodoState> {
  private todoStore = createStore<TodoState>({ todos: [] }, actions, {
    middleware: [loggerMiddleware(), persistMiddleware('todos')],
  });
}

Performance Tips

  1. Use Signals for frequent updates

    // Fast updates
    count = this.select((state) => state.count);
  2. Use Observables for async operations

    // Works with RxJS operators
    todos$ = this.select$((state) => state.todos).pipe(debounceTime(300), distinctUntilChanged());
  3. Combine with computed()

    filtered = computed(() => {
      // Automatically tracks dependencies
      return this.todos().filter(...);
    });
  4. Avoid multiple signals in template

    // Single call
    state = this.select((state) => state);
    
    // Better
    count = this.select((state) => state.count);
    todos = this.select((state) => state.todos);

Comparison with Other Solutions

vs NgRx

  • Simpler API
  • Smaller bundle size
  • Works with RxJS and Signals
  • No boilerplate

vs Akita

  • Framework-agnostic core
  • Better TypeScript inference
  • Easier to test

vs Elf

  • More familiar API
  • Works in React too
  • Built-in async support

Contributing

See CONTRIBUTING.md

License

MIT