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

@tcn/state

v1.3.2

Published

State management for complex business logic.

Readme

@tcn/state

A lightweight, type-safe state management library for TypeScript applications.

Table of Contents

Installation

npm install @tcn/state
# or
yarn add @tcn/state
# or
pnpm add @tcn/state

Entry Points

The library provides multiple entry points to optimize your bundle size:

| Entry Point | Description | React Required | |-------------|-------------|----------------| | @tcn/state | Everything (backward compatible) | Yes | | @tcn/state/core | Core utilities only (Signal, Runner, events) | No | | @tcn/state/react | React hooks only | Yes |

Importing Core Utilities (No React)

Use this when you need state management in non-React contexts or want to avoid bundling React:

import { Signal, Runner, Event } from '@tcn/state/core';

Importing React Hooks

Use this when you only need the React integration:

import { useSignalValue, useRunnerStatus } from '@tcn/state/react';

Importing Everything

For backward compatibility, the main entry point exports everything:

import { Signal, Runner, useSignalValue } from '@tcn/state';

Quick Start

Basic Counter Example

// CounterPresenter.ts
class CounterPresenter {
  private _countSignal: Signal<number>;

  get countBroadcast() {
    return this._countSignal.broadcast;
  }

  constructor() {
    this._countSignal = new Signal<number>(0);
  }

  increment() {
    this._countSignal.transform(count => count + 1);
  }

  decrement() {
    this._countSignal.transform(count => count - 1);
  }

  dispose() {
    this._countSignal.dispose();
  }
}

// Counter.tsx
function Counter({ presenter }: { presenter: CounterPresenter }) {
  const count = useSignalValue(presenter.countBroadcast);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => presenter.increment()}>Increment</button>
      <button onClick={() => presenter.decrement()}>Decrement</button>
    </div>
  );
}

Core Concepts

The library provides two main classes for state management:

  1. Signal: Base class for reactive state management

    • Manages a single value of type T
    • Notifies subscribers when the value changes
    • Provides memory-efficient updates through transform
  2. Runner: Extends Signal for handling async operations

    • Manages async operation state (INITIAL, PENDING, SUCCESS, ERROR)
    • Provides progress tracking and error handling
    • Supports retry and reset operations

Using Signals

Signals are designed to be encapsulated within classes, providing controlled access to state through readonly interfaces.

Basic Usage

class TodoListPresenter {
  private _todosSignal: Signal<Todo[]>;
  private _completedTodosSignal: Signal<number>;

  get todosBroadcast() {
    return this._todosSignal.broadcast;
  }

  get completedCountBroadcast() {
    return this._completedTodosSignal.broadcast;
  }

  constructor() {
    this._todosSignal = new Signal<Todo[]>([]);
    this._completedTodosSignal = new Signal<number>(0);

    this._todosSignal.subscribe(todos => {
      this._completedTodosSignal.set(
        todos.filter(todo => todo.completed).length
      );
    });
  }

  dispose() {
    this._todosSignal.dispose();
    this._completedTodosSignal.dispose();
  }
}

Using Runners

Runners provide a powerful way to manage asynchronous operations with built-in state management.

Status Types

  1. INITIAL: Default state, no operation running
  2. PENDING: Operation in progress, progress can be updated
  3. SUCCESS: Operation completed successfully
  4. ERROR: Operation failed, contains error information

Basic Usage

class DataServicePresenter {
  private _dataRunner: Runner<Data>;

  get dataBroadcast() {
    return this._dataRunner.broadcast;
  }

  constructor() {
    this._dataRunner = new Runner<Data>(null);
  }

  async fetchData() {
    await this._dataRunner.execute(async () => {
      const response = await fetch('/api/data');
      return await response.json();
    });
  }

  dispose() {
    this._dataRunner.dispose();
  }
}

React Integration

Presenter Patterns

  1. Root Presenter Pattern (Recommended)

    class AppPresenter {
      readonly userPresenter: UserPresenter;
         
      constructor() {
        this.userPresenter = new UserPresenter();
      }
         
      dispose() {
        this.userPresenter.dispose();
      }
    }
  2. Local State Pattern (For isolated components)

    function MyComponent() {
      const [presenter] = useState(() => new MyPresenter());
         
      useEffect(() => {
        return () => presenter.dispose();
      }, [presenter]);
         
      return <div>...</div>;
    }

React Hooks

  • useSignalValue<T>(broadcast: IBroadcast<T>): T
  • useRunnerStatus<T>(broadcast: IRunnerBroadcast<T>): Status
  • useRunnerProgress<T>(broadcast: IRunnerBroadcast<T>): number
  • useRunnerError<T>(broadcast: IRunnerBroadcast<T>): Error | null

API Reference

Signal

Methods

  • set(value: T): void
  • transform(cb: (val: T) => T): void
  • subscribe(callback: (value: T) => void): ISubscription
  • dispose(): void

Runner

Methods

  • execute(action: () => Promise<T>): Promise
  • dispatch(action: () => Promise<T>): Promise
  • retry(): Promise
  • reset(): void
  • setProgress(progress: number): void
  • setFeedback(feedback: string): void
  • setError(error: Error | null): void
  • dispose(): void

Troubleshooting

  1. Memory Management

    • Its advised to call dispose() on signals and runners when they're no longer needed, but not necessary because Signals subscriptions are WeakRefs
    • When using the Root Presenter Pattern (injecting presenters through props), DO NOT dispose the presenter in the component
    • When using the Local State Pattern (creating presenters with useState), you MUST dispose the presenter in the component's cleanup function
  2. Performance

    • Use transform for memory-efficient updates
    • Avoid creating new arrays/objects when updating state
    • Don't create new signals in render methods
  3. Type Safety

    • Always specify generic types for signals and runners
    • Use TypeScript's type inference when possible
    • Maintain type consistency across your application

Examples

Real-time Data Updates

import { Signal, Runner } from '@tcn/state/core';

class StockPricePresenter {
  private _priceSignal: Signal<number>;
  private _updateRunner: Runner<void>;
  private _ws: WebSocket | null;
  private _symbol: string;

  get priceBroadcast() {
    return this._priceSignal.broadcast;
  }

  get updateRunnerBroadcast() {
    return this._updateRunner.broadcast;
  }

  constructor(symbol: string) {
    this._symbol = symbol;
    this._priceSignal = new Signal<number>(0);
    this._updateRunner = new Runner<void>();
    this._ws = null;
  }

  async initialize() {
    try {
      this._ws = new WebSocket(`wss://api.example.com/stock/${this._symbol}`);
      
      // Handle WebSocket connection
      this._ws.onopen = () => {
        console.log('WebSocket connected');
      };

      // Handle WebSocket messages
      this._ws.onmessage = (event) => {
        const price = JSON.parse(event.data).price;
        this._priceSignal.set(price);
      };

      // Handle WebSocket errors
      this._ws.onerror = (error) => {
        console.error('WebSocket error:', error);
        this._updateRunner.setError(new Error('WebSocket connection failed'));
      };

      // Handle WebSocket closure
      this._ws.onclose = () => {
        console.log('WebSocket disconnected');
      };

      return true;
    } catch (error) {
      console.error('Failed to initialize WebSocket:', error);
      this._updateRunner.setError(new Error('Failed to initialize WebSocket connection'));
      return false;
    }
  }

  async refresh() {
    await this._updateRunner.dispatch(async () => {
      const response = await fetch(`/api/stock/${this._symbol}`);
      const data = await response.json();
      this._priceSignal.set(data.price);
    });
  }

  dispose() {
    this._ws?.close();
    this._priceSignal.dispose();
    this._updateRunner.dispose();
  }
}

// Usage in React component
function StockPriceView({ presenter }: { presenter: StockPricePresenter }) {
  const price = useSignalValue(presenter.priceBroadcast);
  const status = useRunnerStatus(presenter.updateRunnerBroadcast);
  const error = useRunnerError(presenter.updateRunnerBroadcast);

  useEffect(() => {
    // Initialize WebSocket connection when component mounts
    presenter.initialize();

    // Cleanup when component unmounts
    return () => {
      presenter.dispose();
    };
  }, []);

  if (status === 'ERROR') {
    return (
      <div>
        <p>Error: {error?.message}</p>
        <button onClick={() => presenter.initialize()}>Retry Connection</button>
      </div>
    );
  }

  return (
    <div>
      <h2>Stock Price: ${price}</h2>
      <button onClick={() => presenter.refresh()}>Refresh Price</button>
    </div>
  );
}

Presenter Composition

// AppPresenter.ts
class AppPresenter {
  // Pattern 1: Readonly property for permanent presenters
  // - Used when the child presenter is always needed
  // - The child presenter is created once and lives as long as the parent
  // - Access is direct and type-safe
  readonly toolbarPresenter: ToolbarPresenter;

  // Pattern 2: Signal for dynamic presenters
  // - Used when the child presenter may come and go
  // - The child presenter can be created and disposed on demand
  // - Access requires checking for null
  private _sidebarSignal: Signal<SidebarPresenter | null>;

  get sidebarBroadcast() {
    return this._sidebarSignal.broadcast;
  }

  constructor() {
    // Pattern 1: Initialize permanent presenters in constructor
    this.toolbarPresenter = new ToolbarPresenter();
    
    // Pattern 2: Initialize signal with null for dynamic presenters
    this._sidebarSignal = new Signal<SidebarPresenter | null>(null);
  }

  toggleSidebar() {
    if (this._sidebarSignal.get() === null) {
      // Pattern 2: Create new presenter when needed
      this._sidebarSignal.set(new SidebarPresenter());
    } else {
      // Pattern 2: Clean up and remove presenter when no longer needed
      this._sidebarSignal.get()?.dispose();
      this._sidebarSignal.set(null);
    }
  }

  dispose() {
    // Pattern 1: Clean up permanent presenters
    this.toolbarPresenter.dispose();
    
    // Pattern 2: Clean up dynamic presenters if they exist
    this._sidebarSignal.get()?.dispose();
    this._sidebarSignal.dispose();
  }
}

// App.tsx
function App() {
  const [appPresenter] = useState(() => new AppPresenter());
  const sidebarPresenter = useSignalValue(appPresenter.sidebarBroadcast);

  useEffect(() => {
    return () => appPresenter.dispose();
  }, [appPresenter]);

  return (
    <div className="app">
      {/* Pattern 1: Direct access to permanent presenter */}
      <Toolbar presenter={appPresenter.toolbarPresenter} />
      
      <div className="content">
        <button onClick={() => appPresenter.toggleSidebar()}>
          {sidebarPresenter ? 'Hide Sidebar' : 'Show Sidebar'}
        </button>
        
        {/* Pattern 2: Conditional rendering based on presenter existence */}
        {sidebarPresenter && (
          <Sidebar presenter={sidebarPresenter} />
        )}
      </div>
    </div>
  );
}

Presenter Composition Patterns

The library supports two main patterns for composing presenters:

1. Permanent Presenters (Readonly Properties)

class ParentPresenter {
  // Child presenter is always available
  readonly childPresenter: ChildPresenter;
  
  constructor() {
    this.childPresenter = new ChildPresenter();
  }
}

Use this pattern when:

  • The child presenter is always needed
  • The child's lifecycle matches the parent's
  • You need direct, type-safe access to the child

2. Dynamic Presenters (Signals)

class ParentPresenter {
  private _childSignal: Signal<ChildPresenter | null>;
  
  get childBroadcast() {
    return this._childSignal.broadcast;
  }
  
  constructor() {
    this._childSignal = new Signal<ChildPresenter | null>(null);
  }
  
  toggleChild() {
    if (this._childSignal.get() === null) {
      this._childSignal.set(new ChildPresenter());
    } else {
      this._childSignal.get()?.dispose();
      this._childSignal.set(null);
    }
  }
}

Use this pattern when:

  • The child presenter may come and go
  • The child's lifecycle is independent of the parent
  • You need to conditionally render components based on the child's existence

Choosing Between Patterns

  1. Use Permanent Presenters when:

    • The child is a core part of the parent's functionality
    • The child's state needs to persist as long as the parent exists
    • You need direct access to the child's methods and properties
  2. Use Dynamic Presenters when:

    • The child is optional or can be toggled
    • The child's state can be discarded when not needed
    • You want to save memory by disposing of unused presenters
    • The child's existence affects the UI layout

Best Practices

  1. Memory Management:

    • Always dispose of presenters when they're no longer needed
    • For permanent presenters, dispose them in the parent's dispose method
    • For dynamic presenters, dispose them before setting the signal to null
  2. Type Safety:

    • Use TypeScript's type system to ensure proper access to presenters
    • For dynamic presenters, always check for null before accessing
  3. Component Integration:

    • Use useSignalValue to subscribe to dynamic presenter signals
    • Pass permanent presenters directly as props
    • Use conditional rendering for dynamic presenters