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

synstate

v0.1.2

Published

Type-safe State Management Library for TypeScript/JavaScript

Readme

SynState

npm version npm downloads License codecov

SynState is a lightweight, high-performance, type-safe state management library for TypeScript/JavaScript applications. Perfect for building reactive global state and event-driven systems in React, Vue, and other frameworks.

Features

  • 🎯 Simple State Management: Easy-to-use createState and createReducer for global state
  • 📡 Event System: Built-in createValueEmitter, createEventEmitter for event-driven architecture
  • 🔄 Reactive Updates: Automatic propagation of state changes to all subscribers
  • 🎨 Type-Safe: Full TypeScript support with precise type inference
  • 🚀 Lightweight: Minimal bundle size with only one external runtime dependency (ts-data-forge)
  • High Performance: Optimized for fast state updates and minimal re-renders
  • 🌐 Framework Agnostic: Works with React, Vue, Svelte, or vanilla JavaScript
  • 🔧 Observable-based: Built on Observable pattern similar to RxJS, but with a completely independent implementation from scratch — not a wrapper. Offers optional advanced features like operators (map, filter, scan, debounceTime) and combinators (merge, combine)

Documentation

  • API reference: TBD

Installation

npm add synstate

Or with other package managers:

# Yarn
yarn add synstate

# pnpm
pnpm add synstate

Quick Start

Simple State Management

// Create a reactive state
const [state, setState, { updateState, resetState, getSnapshot }] =
    createState(0);

const mut_history: number[] = [];

// Subscribe to changes (in React components, Vue watchers, etc.)
state.subscribe((count) => {
    mut_history.push(count);
});

assert.deepStrictEqual(mut_history, [0]);

// Update state
setState(1);

assert.deepStrictEqual(mut_history, [0, 1]);

updateState((prev) => prev + 2);

assert.deepStrictEqual(mut_history, [0, 1, 3]);

assert.isTrue(getSnapshot() === 3);

resetState();

assert.isTrue(getSnapshot() === 0);

With React

import * as React from 'react';
import { createState } from 'synstate';

// Global state (outside component)
const [userState, setUserState, { getSnapshot }] = createState({
    name: '',
    email: '',
});

const UserProfile = (): React.JSX.Element => {
    const [user, setUser] = React.useState(getSnapshot());

    React.useEffect(() => {
        const subscription = userState.subscribe(setUser);

        return () => {
            subscription.unsubscribe();
        };
    }, []);

    return (
        <div>
            <p>
                {'Name: '}
                {user.name}
            </p>
            <button
                onClick={() => {
                    setUserState({
                        name: 'Alice',
                        email: '[email protected]',
                    });
                }}
            >
                {'Set User'}
            </button>
        </div>
    );
};

If you're using React v18 or later:

import * as React from 'react';
import { createState } from 'synstate';

const [userState, setUserState] = createState({
    name: '',
    email: '',
});

const UserProfile = (): React.JSX.Element => {
    const user = React.useSyncExternalStore(
        (onStoreChange: () => void) => {
            const { unsubscribe } = userState.subscribe(onStoreChange);

            return unsubscribe;
        },
        () => userState.getSnapshot().value,
    );

    return (
        <div>
            <p>
                {'Name: '}
                {user.name}
            </p>
            <button
                onClick={() => {
                    setUserState({
                        name: 'Alice',
                        email: '[email protected]',
                    });
                }}
            >
                {'Set User'}
            </button>
        </div>
    );
};

You can write the equivalent code more concisely using synstate-react-hooks:

npm add synstate-react-hooks
import type * as React from 'react';
import { createState } from 'synstate-react-hooks';

const [useUserState, setUserState] = createState({
    name: '',
    email: '',
});

const UserProfile = (): React.JSX.Element => {
    const user = useUserState();

    return (
        <div>
            <p>
                {'Name: '}
                {user.name}
            </p>
            <button
                onClick={() => {
                    setUserState({
                        name: 'Alice',
                        email: '[email protected]',
                    });
                }}
            >
                {'Set User'}
            </button>
        </div>
    );
};

See also the synstate-react-hooks README.

Core Concepts

State Management

SynState provides simple, intuitive APIs for managing application state:

  • createState: Create state with getter/setter
  • createReducer: Create state by reducer and initial value
  • createBooleanState: Specialized state for boolean values

Event System

Built-in event emitter for event-driven patterns:

  • createValueEmitter: Create type-safe event emitters
  • createEventEmitter: Create event emitters without payload

Observable (Optional Advanced Feature)

For advanced use cases, you can use observables to build complex reactive data flows. However, most applications will only need createState, createReducer, and createValueEmitter.

API Reference

For complex scenarios, SynState provides observable-based APIs:

Creation Functions

  • source<T>(): Create a new observable source
  • of(value): Create observable from a single value
  • fromArray(array): Create observable from array
  • fromPromise(promise): Create observable from promise
  • interval(ms): Emit values at intervals
  • timer(delay): Emit after delay

Operators

  • filter(predicate): Filter values
  • map(fn): Transform values
  • scan(reducer, seed): Accumulate values
  • debounceTime(ms): Debounce emissions
  • throttleTime(ms): Throttle emissions
  • skipIfNoChange(): Skip duplicate values
  • takeUntil(notifier): Complete on notifier emission

Combination

  • combine(observables): Combine latest values from multiple sources
  • merge(observables): Merge multiple streams
  • zip(observables): Pair values by index

Examples

Global Counter State (React)

import type * as React from 'react';
import { createState } from 'synstate-react-hooks';

// Create global state
export const [useCounterState, , { updateState, resetState, getSnapshot }] =
    createState(0);

// Component 1
const Counter = (): React.JSX.Element => {
    const count = useCounterState();

    return (
        <div>
            <p>
                {'Count: '}
                {count}
            </p>
            <button
                onClick={() => {
                    updateState((n: number) => n + 1);
                }}
            >
                {'Increment'}
            </button>
        </div>
    );
};

// Component 2 (synced automatically)
const ResetButton = (): React.JSX.Element => (
    <button
        onClick={() => {
            resetState();
        }}
    >
        {'Reset'}
    </button>
);

Todo List with Reducer (React)

import * as React from 'react';
import { createReducer } from 'synstate-react-hooks';

type Todo = Readonly<{
    id: number;
    text: string;
    done: boolean;
}>;

type Action = Readonly<
    | { type: 'add'; text: string }
    | { type: 'toggle'; id: number }
    | { type: 'remove'; id: number }
>;

const initialTodos: readonly Todo[] = [];

const reducer = (todos: readonly Todo[], action: Action): readonly Todo[] => {
    switch (action.type) {
        case 'add':
            return [
                ...todos,
                {
                    id: Date.now(),
                    text: action.text,
                    done: false,
                },
            ];

        case 'toggle':
            return todos.map((t) =>
                t.id === action.id ? { ...t, done: !t.done } : t,
            );

        case 'remove':
            return todos.filter((t) => t.id !== action.id);
    }
};

const [useTodoState, dispatch] = createReducer<readonly Todo[], Action>(
    reducer,
    initialTodos,
);

const addTodo = (): void => {
    dispatch({
        type: 'add',
        text: 'New Todo',
    });
};

const TodoList = (): React.JSX.Element => {
    const todos = useTodoState();

    const todosWithHandler = React.useMemo(
        () =>
            todos.map((todo) => ({
                ...todo,
                onToggle: () => {
                    dispatch({
                        type: 'toggle',
                        id: todo.id,
                    });
                },
                onRemove: () => {
                    dispatch({
                        type: 'remove',
                        id: todo.id,
                    });
                },
            })),
        [todos],
    );

    return (
        <div>
            {todosWithHandler.map((todo) => (
                <div key={todo.id}>
                    <input
                        checked={todo.done}
                        type={'checkbox'}
                        onChange={todo.onToggle}
                    />
                    <span>{todo.text}</span>
                    <button onClick={todo.onRemove}>{'Remove'}</button>
                </div>
            ))}
            <button onClick={addTodo}>{'Add Todo'}</button>
        </div>
    );
};

Boolean State (Dark Mode)

import * as React from 'react';
import { createBooleanState } from 'synstate-react-hooks';

export const [useDarkModeState, { toggle: toggleDarkMode }] =
    createBooleanState(false);

const ThemeToggle = (): React.JSX.Element => {
    const isDark = useDarkModeState();

    React.useEffect(() => {
        document.body.className = isDark ? 'dark' : 'light';
    }, [isDark]);

    return <button onClick={toggleDarkMode}>{isDark ? '🌙' : '☀️'}</button>;
};

Cross-Component Communication

import * as React from 'react';
import { createState } from 'synstate-react-hooks';

// State
const [useItemsState, _, { updateState, resetState: resetItemsState }] =
    createState<readonly string[]>([]);

// Setup event handlers
const addItem = (item: string): void => {
    updateState((items: readonly string[]) => [...items, item]);
};

// Component 1: Add items
const ItemInput = (): React.JSX.Element => {
    const [input, setInput] = React.useState<string>('');

    return (
        <div>
            <input
                value={input}
                onChange={(e) => {
                    setInput(e.target.value);
                }}
            />
            <button
                onClick={() => {
                    addItem(input);

                    setInput('');
                }}
            >
                {'Add'}
            </button>
        </div>
    );
};

// Component 2: Display items
const ItemList = (): React.JSX.Element => {
    const items = useItemsState();

    return (
        <div>
            <ul>
                {items.map((item, i) => (
                    <li key={i}>{item}</li>
                ))}
            </ul>
            <button onClick={resetItemsState}>{'Clear All'}</button>
        </div>
    );
};

// Events

Advanced: Search with Debounce

import type * as React from 'react';
import {
    createState,
    debounceTime,
    filter,
    fromPromise,
    type InitializedObservable,
    map,
    switchMap,
    withInitialValue,
} from 'synstate';
import { useObservableValue } from 'synstate-react-hooks';
import { Result } from 'ts-data-forge';

const [searchState, setSearchState] = createState('');

// Advanced reactive pipeline with debounce and filtering
const searchResults$: InitializedObservable<
    readonly Readonly<{ id: string; name: string }>[]
> = searchState
    .pipe(debounceTime(300))
    .pipe(filter((query) => query.length > 2))
    .pipe(
        switchMap((query) =>
            fromPromise(
                fetch(`/api/search?q=${query}`).then(
                    (r) =>
                        r.json() as Promise<
                            readonly Readonly<{ id: string; name: string }>[]
                        >,
                ),
            ),
        ),
    )
    .pipe(filter((res) => Result.isOk(res)))
    .pipe(map((res) => Result.unwrapOk(res)))
    .pipe(withInitialValue([]));

const SearchBox = (): React.JSX.Element => {
    const searchResults = useObservableValue(searchResults$);

    return (
        <div>
            <input
                placeholder={'Search...'}
                onChange={(e) => {
                    setSearchState(e.target.value);
                }}
            />
            <ul>
                {searchResults.map((item) => (
                    <li key={item.id}>{item.name}</li>
                ))}
            </ul>
        </div>
    );
};

Advanced: Event Emitter with Throttle

import { createEventEmitter, throttleTime } from 'synstate';

// Create event emitter
const [refreshClicked, onRefreshClick] = createEventEmitter();

// Subscribe to events
refreshClicked.subscribe(() => {
    console.log('Refresh Clicked');
});

// Throttle refresh clicks to prevent rapid successive executions
const throttledRefresh = refreshClicked.pipe(throttleTime(2000));

throttledRefresh.subscribe(() => {
    console.log('Executing refresh...');
    // Actual refresh logic here
    // This will be called at most once every 2 seconds
});

const DataTable = (): React.JSX.Element => (
    <div>
        <button onClick={onRefreshClick}>{'Refresh'}</button>
        <p>
            {'Data: '}
            {/* Display data here */}
        </p>
    </div>
);

Why SynState?

Simple State Management, Not Complex Reactive Programming

SynState is a state management library for web frontends, similar to Redux, Jotai, Zustand, and MobX. It provides APIs for creating and managing global state across your application.

Under the hood, SynState is built on Observable patterns similar to those provided by RxJS. However, unlike RxJS, which can make code harder to read with many operators and complex streams, SynState focuses on simple, readable state management and event handling. Most applications only need createState, createReducer, and simple operators/combinators like combine and map — clean, straightforward APIs that developers understand immediately.

Advanced reactive features are optional and only used when you actually need them (like debouncing search input). The library doesn't force you into a reactive programming mindset.

Key Differences from RxJS

  • Focus on State Management: Designed specifically for state management, not just asynchronous event processing
  • InitializedObservable: Provides InitializedObservable which always holds an initial value, making it ideal for representing state
  • Simpler API: Most use cases are covered by createState, createReducer, and createEventEmitter
  • Better Readability: No need for complex operator chains in everyday code
  • Optional Complexity: Advanced features available to manipulate Observables when needed

Use Cases

Use SynState when you need:

  • ✅ Global state management across components
  • ✅ Event-driven communication between components
  • ✅ Type-safe event emitters
  • ✅ Redux-like state with reducers
  • ✅ Simple reactive patterns (debounce, throttle, etc.)

Consider other solutions when:

  • ❌ You need state in a React component (use React hooks useState, useReducer)
  • ❌ Your app is simple enough for React Context alone

Type Safety

SynState maintains full type information.

License

This project is licensed under the Apache License 2.0.

Repository

https://github.com/noshiro-pf/synstate