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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@masatomakino/state-atoms

v0.2.2

Published

StateAtoms is a lightweight state management library for TypeScript.

Readme

StateAtoms

A lightweight state management library for TypeScript that divides application state into small, reactive units called "Atoms". StateAtoms provides event-driven state management with serialization capabilities and optional undo/redo functionality.

MIT License test

Features

  • Reactive State Management: Automatic change detection and event emission
  • Hierarchical Containers: Organize atoms into containers with event propagation
  • Deep Equality Support: ObjectAtom with intelligent change detection for complex objects
  • Serialization: Built-in JSON serialization and deserialization
  • History Management: Optional undo/redo functionality
  • TypeScript Support: Full type safety with explicit event type specification
  • Lightweight: Minimal dependencies (eventemitter3, fast-equals)

Overview

StateAtoms divides state into small units called Atoms, which can be combined into containers to represent complex application state. This modular approach makes it easy to manage and monitor state changes with fine-grained control.

Installation

You can install StateAtoms using npm. Run the following command:

npm install @masatomakino/state-atoms

Usage

Basic Atom

To use StateAtoms, first import the necessary modules:

import { Atom } from "@masatomakino/state-atoms";

Create an atom with an initial value (type is automatically inferred):

const countAtom = new Atom(0); // Type: Atom<number>
const nameAtom = new Atom("John"); // Type: Atom<string>

Update the atom value:

countAtom.value = 1; // Triggers change events
countAtom.value = 1; // No events (same value)

Listen for changes:

countAtom.on("change", (args) => {
  console.log(`Value changed: ${args.valueFrom} -> ${args.value}`);
});

countAtom.on("beforeChange", (args) => {
  console.log(`About to change: ${args.valueFrom} -> ${args.value}`);
});

ObjectAtom for Complex Data

For objects that require deep equality comparison, use ObjectAtom:

import { ObjectAtom } from "@masatomakino/state-atoms";

interface User {
  name: string;
  age: number;
}

const userAtom = new ObjectAtom<User>({ name: "John", age: 30 });

// This will NOT trigger an event (structurally identical)
userAtom.value = { name: "John", age: 30 };

// This WILL trigger an event (different values)
userAtom.value = { name: "Jane", age: 25 };

AtomContainer for State Management

AtomContainer manages multiple atoms with automatic event propagation:

import { AtomContainer, Atom, ObjectAtom } from "@masatomakino/state-atoms";

interface AppState {
  user: { name: string; age: number };
  count: number;
  settings: { theme: string };
}

class AppContainer extends AtomContainer<AppState> {
  user = new ObjectAtom({ name: "John", age: 30 });
  count = new Atom(0);
  settings = new ObjectAtom({ theme: "light" });

  constructor() {
    super();
    this.connectMemberAtoms(); // Required after adding member atoms
  }
}

Use the container:

const app = new AppContainer();

// Listen to all changes in the container
// Use explicit type specification for type safety
app.on("change", (args) => {
  console.log(`Changed: ${args.from.constructor.name}`);
  console.log(`Value: ${args.valueFrom} -> ${args.value}`);
  
  // Recommended: Use explicit typing or type guards
  if (args.from === app.count) {
    console.log(`Count changed to: ${args.value as number}`);
  }
});

// Changes to any atom trigger container events
app.count.value = 5;
app.user.value = { name: "Jane", age: 25 };

Serialization and State Persistence

// Serialize to JSON
const stateJson = app.toJson();
console.log(stateJson); // '{"user":{"name":"Jane","age":25},"count":5,"settings":{"theme":"light"}}'

// Serialize to object
const stateObj = app.toObject();

// Restore from data
app.fromObject({ count: 10, user: { name: "Bob", age: 40 } });
app.fromJson('{"count":15}');

History and Undo/Redo

StateAtoms uses manual history management to provide optimal user experience. History snapshots are created at meaningful interaction boundaries, not on every value change.

class HistoryContainer extends AtomContainer<{ count: number }> {
  count = new Atom(0);

  constructor() {
    super({ useHistory: true }); // Enable history tracking
    this.connectMemberAtoms();
  }
}

const container = new HistoryContainer();

// Manual history management for meaningful undo points
container.count.value = 1;
container.count.emit('addHistory'); // Save meaningful state

container.count.value = 2;
container.count.emit('addHistory'); // Save another meaningful state

container.count.value = 3;
container.count.emit('addHistory'); // Save final state

console.log(container.count.value); // 3

container.undo();
console.log(container.count.value); // 2

container.undo();
console.log(container.count.value); // 1

container.redo();
console.log(container.count.value); // 2

Why Manual History? Manual addHistory events allow applications to control when meaningful snapshots are created, resulting in intuitive undo/redo behavior that matches user expectations. For comprehensive UI integration patterns, see guides/ui-integration-patterns.md.

Advanced Configuration

// Skip serialization for different use cases
const sessionToken = new Atom("secret-token", {
  skipSerialization: true // Sensitive data
});

const currentModalState = new Atom("dialog-open", {
  skipSerialization: true // Temporary UI state that resets on navigation
});

const loadingState = new Atom(false, {
  skipSerialization: true // Runtime state not needed for backend sync
});

// Container with mixed serialization needs
class AppContainerWithRuntimeState extends AtomContainer<{
  userSettings: { theme: string; language: string };
}> {
  // Persistent application data
  userSettings = new ObjectAtom({ theme: "light", language: "en" });
  
  // Temporary UI state (excluded from serialization/backend sync)
  isMenuOpen = new Atom(false, { skipSerialization: true });
  currentPage = new Atom("home", { skipSerialization: true });
  
  // Sensitive data (excluded from serialization)
  authToken = new Atom("", { skipSerialization: true });

  constructor() {
    super({ useHistory: true });
    this.connectMemberAtoms();
  }
}

API Reference

Atom<T>

Holds primitive values with change notifications.

  • constructor(initialValue: T, options?): Create a new atom
  • value: T: Get/set the current value
  • skipSerialization: boolean: Whether to exclude from serialization (default: false)

Events:

  • change: Emitted when value changes
  • beforeChange: Emitted before value changes

ObjectAtom<T>

Extends Atom with deep equality comparison for objects.

  • Uses fast-equals for structural comparison
  • Same API as Atom but with smarter change detection

AtomContainer<DataType, EventTypes?>

Manages multiple atoms with hierarchical event propagation and explicit event type handling.

Type Parameters:

  • DataType: The structure of data when serialized (e.g., { count: number; name: string })
  • EventTypes: Event types (defaults to AtomEvents<unknown> for maximum flexibility)

Event Type Handling: EventTypes defaults to AtomEvents<unknown> due to the dynamic nature of event bubbling. For type safety, use explicit type specification in event handlers or type guards to handle different atom types appropriately.

Methods:

  • constructor(options?): Create container with optional history
  • toObject(): Serialize to plain object
  • toJson(): Serialize to JSON string
  • fromObject(obj): Restore from object
  • fromJson(json): Restore from JSON
  • load(obj): Load data and reset history
  • undo(): Undo last change (if history enabled)
  • redo(): Redo last undone change (if history enabled)
  • addHistory(): Manually add current state to history

Events:

  • change: Propagated from child atoms/containers (typed based on DataType)
  • beforeChange: Propagated from child atoms/containers (typed based on DataType)
  • addHistory: Emitted when history entry is added

Event Handling Examples:

// Recommended: Explicit type specification in event handlers
class MyContainer extends AtomContainer<{ count: number; active: boolean }> {
  count = new Atom(0);
  active = new Atom(false);
  
  constructor() {
    super();
    this.connectMemberAtoms();
    
    // Pattern 1: Explicit type specification
    this.on("change", (args: AtomEventArgs<number>) => {
      if (args.from === this.count) {
        console.log(`Count: ${args.value}`);
      }
    });
    
    // Pattern 2: Type guards
    this.on("change", (args) => {
      if (typeof args.value === 'number') {
        console.log(`Number value: ${args.value}`);
      }
    });
  }
}

// Custom events with explicit EventTypes
interface CustomEvents extends AtomEvents<unknown> {
  customEvent: (data: string) => void;
}
class CustomContainer extends AtomContainer<{ count: number; active: boolean }, CustomEvents> {
  // Custom event types with explicit specification
}

Development

Building

npm run build

Testing

npm test          # Run all tests
npm run coverage  # Run tests with coverage

Code Quality

npx biome check   # Lint and format check
npx biome format  # Format code

License

This project is licensed under the MIT License - see the LICENSE file for details.