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

signalforge

v1.0.1

Published

Fine-grained reactive state management with automatic dependency tracking - Ultra-optimized, zero dependencies

Readme

SignalForge

SignalForge logo

Fine-Grained Reactive State Management for Modern JavaScript

npm version License: MIT Bundle Size TypeScript

Quick StartExamplesDocsAPI Reference


What is SignalForge?

A simple state management library for React and React Native. Your UI automatically updates when data changes. No Redux complexity, no boilerplate.

Think of it as smart variables:

const count = createSignal(0);         // Create a signal
count.set(5);                          // Update it
count.get();                           // Read it: 5

// Computed values update automatically
const doubled = createComputed(() => count.get() * 2);
console.log(doubled.get());            // 10

Why SignalForge?

Easy to Learn

Only 3 functions to master:

createSignal(value)              // Store data
createComputed(() => ...)        // Auto-calculate from other signals
createEffect(() => ...)          // Run code when signals change

Works Everywhere

  • React (hooks + class components)
  • React Native (iOS + Android)
  • Next.js (SSR ready)
  • Plain JavaScript

Actually Fast

  • 2KB total bundle size
  • Updates 33x faster than individual changes
  • Handles thousands of signals smoothly

Batteries Included

  • Auto-save to storage (localStorage/AsyncStorage)
  • DevTools for debugging
  • Time travel (undo/redo)
  • No dependencies

Quick Start

1. Install

npm install signalforge

2. Use in React

import { useSignal } from 'signalforge/react';

function Counter() {
  const [count, setCount] = useSignal(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

That's it! No providers, no context, no configuration needed.

3. Share State Between Components

Create signals outside components for global state:

import { createSignal } from 'signalforge';
import { useSignalValue } from 'signalforge/react';

// Global signal - accessible anywhere
const userPoints = createSignal(0);

function Header() {
  const points = useSignalValue(userPoints);
  return <div>Points: {points}</div>;
}

function AddPointsButton() {
  return (
    <button onClick={() => userPoints.set(userPoints.get() + 10)}>
      Add Points
    </button>
  );
}

// Both components stay in sync automatically!

4. Auto-Calculated Values

import { createSignal, createComputed } from 'signalforge';

const price = createSignal(100);
const quantity = createSignal(2);

// Total updates automatically when price or quantity changes
const total = createComputed(() => price.get() * quantity.get());

price.set(150);
console.log(total.get());  // 300 (auto-updated!)

Core Concepts

Signals - Store Data

import { createSignal } from 'signalforge';

const count = createSignal(0);

count.get();                    // Read: 0
count.set(5);                   // Write: 5
count.set(c => c + 1);          // Update: 6

Computed - Auto-Calculate

import { createComputed } from 'signalforge';

const firstName = createSignal('John');
const lastName = createSignal('Doe');

// Recalculates automatically when firstName or lastName changes
const fullName = createComputed(() => 
  `${firstName.get()} ${lastName.get()}`
);

firstName.set('Jane');
console.log(fullName.get());    // "Jane Doe"

Effects - Run Side Effects

import { createEffect } from 'signalforge';

const username = createSignal('guest');

// Runs automatically when username changes
createEffect(() => {
  console.log('User:', username.get());
  // Save to analytics, update title, etc.
});

Batch Updates - Better Performance

import { batch } from 'signalforge';

// Updates UI only once instead of 3 times
batch(() => {
  signal1.set('a');
  signal2.set('b');
  signal3.set('c');
});

Persist to Storage

import { persist } from 'signalforge/utils';

const theme = createSignal('light');
persist(theme, { key: 'app-theme' });

// Value automatically saved and restored on app restart

React Hooks

useSignal - Local Component State

import { useSignal } from 'signalforge/react';

function TodoApp() {
  const [task, setTask] = useSignal('');
  const [todos, setTodos] = useSignal([]);
  
  const addTodo = () => {
    setTodos([...todos, task]);
    setTask('');
  };
  
  return (
    <div>
      <input value={task} onChange={e => setTask(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      {todos.map(todo => <div key={todo}>{todo}</div>)}
    </div>
  );
}

useSignalValue - Subscribe to Global Signals

import { createSignal } from 'signalforge';
import { useSignalValue } from 'signalforge/react';

// Create global signal
const currentUser = createSignal({ name: 'Guest', points: 0 });

// Use in any component
function UserProfile() {
  const user = useSignalValue(currentUser);
  return <div>{user.name} has {user.points} points</div>;
}

function AnotherComponent() {
  const user = useSignalValue(currentUser);
  // Both components update when currentUser changes!
  return <button onClick={() => currentUser.set({...user, points: user.points + 10})}>
    Add Points
  </button>;
}

function Logger() {
  const [count] = useSignal(0);
  
  useSignalEffect(() => {
    console.log('Count changed:', count);
    // Auto-tracks count dependency!
  });
  
  return <div>Count: {count}</div>;
}

Class Component Support

Use withSignals HOC to inject signal values as props:

import { withSignals } from 'signalforge/react';

class CounterPanel extends React.Component {
  render() {
    return <div>Count: {this.props.count}</div>;
  }
}

export default withSignals(CounterPanel, { 
  count: globalCount 
});

Complete React Example

import { createSignal, createComputed } from 'signalforge';
import { useSignalValue } from 'signalforge/react';

// Global signals
const items = createSignal([
  { id: 1, name: 'Apple', price: 1.5, qty: 2 },
  { id: 2, name: 'Bread', price: 2.0, qty: 1 }
]);

const total = createComputed(() => 
  items.get().reduce((sum, item) => 
    sum + (item.price * item.qty), 0
  )
);

function ShoppingCart() {
  const cartItems = useSignalValue(items);
  const cartTotal = useSignalValue(total);
  
  const addItem = (id) => {
    items.set(current => 
      current.map(item => 
        item.id === id 
          ? { ...item, qty: item.qty + 1 }
          : item
      )
    );
  };
  
  return (
    <div>
      <h2>Shopping Cart</h2>
      {cartItems.map(item => (
        <div key={item.id}>
          {item.name} - ${item.price} x {item.qty}
          <button onClick={() => addItem(item.id)}>+</button>
        </div>
      ))}
      <h3>Total: ${cartTotal.toFixed(2)}</h3>
    </div>
  );
}

React Native

Installation

npm install signalforge @react-native-async-storage/async-storage
cd ios && pod install && cd ..  # iOS only

Usage

Same API as React - just import and use:

import { useSignal } from 'signalforge/react';
import { View, Button, Text } from 'react-native';

function Counter() {
  const [count, setCount] = useSignal(0);
  
  return (
    <View>
      <Text>Count: {count}</Text>
      <Button title="+1" onPress={() => setCount(count + 1)} />
    </View>
  );
}

Save to AsyncStorage

import { createSignal } from 'signalforge';
import { persist } from 'signalforge/utils';
import { useEffect } from 'react';

const settings = createSignal({ theme: 'dark', notifications: true });

function App() {
  useEffect(() => {
    persist(settings, { key: 'app_settings' });
  }, []);
  // Settings now auto-save and restore!
}

Try the Demo App

Complete working example with 19 screens:

git clone https://github.com/forgecommunity/signalforge.git
cd signalforge && npm install && npm run build
cd examples/sfReactNative && npm install
npm start

# In another terminal:
npm run android  # or npm run ios

Includes: Shopping cart, forms, persistence, cross-screen communication, and more.

Copy/paste examples from the React Native demo

All of these are lifted directly from the working examples/sfReactNative screens so you can copy them as-is.

Basic signal (screens/BasicSignalScreen.tsx)

import { Text, TouchableOpacity, View } from 'react-native';
import { createSignal } from 'signalforge';
import { useSignalValue } from 'signalforge/react';

const age = createSignal(25);

export default function BasicSignalScreen() {
  const currentAge = useSignalValue(age);

  return (
    <View>
      <Text>Current Age: {currentAge}</Text>
      <TouchableOpacity onPress={() => age.set(age.get() + 1)}><Text>Increment</Text></TouchableOpacity>
      <TouchableOpacity onPress={() => age.set(age.get() - 1)}><Text>Decrement</Text></TouchableOpacity>
      <TouchableOpacity onPress={() => age.set(25)}><Text>Reset</Text></TouchableOpacity>
    </View>
  );
}

Computed totals (screens/ComputedSignalScreen.tsx)

import { Text, TouchableOpacity, View } from 'react-native';
import { createComputed, createSignal } from 'signalforge';
import { useSignalValue } from 'signalforge/react';

const price = createSignal(100);
const quantity = createSignal(2);
const total = createComputed(() => price.get() * quantity.get());

export default function ComputedSignalScreen() {
  const currentPrice = useSignalValue(price);
  const currentQuantity = useSignalValue(quantity);
  const currentTotal = useSignalValue(total);

  return (
    <View>
      <Text>Total: ${currentTotal}</Text>
      <TouchableOpacity onPress={() => price.set(Math.max(0, currentPrice - 10))}><Text>-10</Text></TouchableOpacity>
      <TouchableOpacity onPress={() => price.set(currentPrice + 10)}><Text>+10</Text></TouchableOpacity>
      <TouchableOpacity onPress={() => quantity.set(Math.max(1, currentQuantity - 1))}><Text>-1 Qty</Text></TouchableOpacity>
      <TouchableOpacity onPress={() => quantity.set(currentQuantity + 1)}><Text>+1 Qty</Text></TouchableOpacity>
    </View>
  );
}

Auto-tracked effects (screens/EffectsScreen.tsx)

import { useEffect, useState } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { createEffect, createSignal } from 'signalforge';
import { useSignalValue } from 'signalforge/react';

const userName = createSignal('John');
const messageCount = createSignal(0);

export default function EffectsScreen() {
  const currentUserName = useSignalValue(userName);
  const currentMessageCount = useSignalValue(messageCount);
  const [logs, setLogs] = useState<string[]>([]);

  useEffect(() => {
    const cleanupName = createEffect(() => {
      const name = userName.get();
      setLogs(prev => [...prev, `👋 Hello, ${name}!`]);
    });

    const cleanupCount = createEffect(() => {
      const count = messageCount.get();
      setLogs(prev => [...prev, `📬 You have ${count} message${count !== 1 ? 's' : ''}`]);
    });

    return () => { cleanupName(); cleanupCount(); };
  }, []);

  return (
    <View>
      <TouchableOpacity onPress={() => userName.set('Jane')}><Text>Change name</Text></TouchableOpacity>
      <TouchableOpacity onPress={() => messageCount.set(messageCount.get() + 1)}><Text>Add message</Text></TouchableOpacity>
      {logs.map((log, i) => <Text key={i}>{log}</Text>)}
      <Text>Current: {currentUserName} / {currentMessageCount}</Text>
    </View>
  );
}

Persisted settings (screens/PersistentSignalScreen.tsx)

import { useEffect, useState } from 'react';
import { Text, TextInput, TouchableOpacity, View } from 'react-native';
import { createSignal } from 'signalforge';
import { persist } from 'signalforge/utils';
import { useSignalValue } from 'signalforge/react';

const username = createSignal('Guest');
const theme = createSignal('light');
const counter = createSignal(0);
let persistInitialized = false;

export default function PersistentSignalScreen() {
  useEffect(() => {
    if (!persistInitialized) {
      persistInitialized = true;
      persist(username, { key: 'demo_username' });
      persist(theme, { key: 'demo_theme' });
      persist(counter, { key: 'demo_counter' });
    }
  }, []);

  const currentUsername = useSignalValue(username);
  const currentTheme = useSignalValue(theme);
  const currentCounter = useSignalValue(counter);
  const [nameInput, setNameInput] = useState('');

  return (
    <View>
      <Text>Current: {currentUsername} ({currentTheme})</Text>
      <TextInput value={nameInput} onChangeText={setNameInput} placeholder="Enter username" />
      <TouchableOpacity onPress={() => theme.set(theme.get() === 'light' ? 'dark' : 'light')}><Text>Toggle Theme</Text></TouchableOpacity>
      <TouchableOpacity onPress={() => counter.set(counter.get() + 1)}><Text>Increment</Text></TouchableOpacity>
      <TouchableOpacity onPress={() => username.set(nameInput || 'Guest')}><Text>Save Username</Text></TouchableOpacity>
      <TouchableOpacity onPress={() => { username.set('Guest'); theme.set('light'); counter.set(0); }}><Text>Reset All</Text></TouchableOpacity>
    </View>
  );
}

Cross-Screen Communication

Signals created outside components work as global state. Multiple screens can read and write the same data:

// shared/userState.ts - Global signals accessible anywhere
import { createSignal, createComputed } from 'signalforge';

export const currentUser = createSignal({ name: 'Guest', points: 0 });
export const cartItems = createSignal([]);
export const totalPoints = createComputed(() => currentUser.get().points);

// screens/ProfileScreen.tsx - Read and write user data
import { useSignalValue } from 'signalforge/react';
import { currentUser } from '../shared/userState';

function ProfileScreen() {
  const user = useSignalValue(currentUser);
  
  const updateName = (newName: string) => {
    currentUser.set({ ...currentUser.get(), name: newName });
  };
  
  return (
    <View>
      <Text>Welcome, {user.name}!</Text>
      <Text>Points: {user.points}</Text>
      <Button title="Add Points" onPress={() => 
        currentUser.set({ ...user, points: user.points + 10 })
      } />
    </View>
  );
}

// screens/CartScreen.tsx - Reads same user, updates when ProfileScreen changes it
import { useSignalValue } from 'signalforge/react';
import { currentUser, cartItems } from '../shared/userState';

function CartScreen() {
  const user = useSignalValue(currentUser);  // Auto-updates when changed elsewhere!
  const items = useSignalValue(cartItems);
  
  return (
    <View>
      <Text>{user.name}'s Cart</Text>
      <Text>You have {user.points} points</Text>
      <Text>{items.length} items in cart</Text>
    </View>
  );
}

// screens/CheckoutScreen.tsx - Also sees live updates
import { useSignalValue } from 'signalforge/react';
import { currentUser, cartItems } from '../shared/userState';

function CheckoutScreen() {
  const user = useSignalValue(currentUser);
  const items = useSignalValue(cartItems);
  
  const completeOrder = () => {
    // Deduct points, clear cart - all screens update automatically!
    currentUser.set({ ...user, points: user.points - 50 });
    cartItems.set([]);
  };
  
  return (
    <View>
      <Text>Checkout for {user.name}</Text>
      <Text>Using {Math.min(50, user.points)} points</Text>
      <Button title="Complete Order" onPress={completeOrder} />
    </View>
  );
}

Why this works:

  • No prop drilling needed
  • All screens update automatically
  • TypeScript ensures type safety
  • Easy to test
  • Simple to understand

Examples

Working example apps you can run and learn from:

Next.js dashboard (examples/sf-nextjs)

  • Shows server-side rendering, client-side hydration, optimistic updates, and persisted theme + auth signals.

  • Try it locally:

    npm install
    npm run build
    cd examples/sf-nextjs && npm install && npm run dev
  • Key files to inspect:

    • components/counter.tsx: global signals shared across routes.
    • lib/session.ts: persisted signals for auth/session data.
    • utils/performance.ts: measuring signal update speed inside React.

React Native starter (examples/sfReactNative)

  • Demonstrates the AsyncStorage persistence adapter, offline-ready counters, and the optional JSI bridge fallback.

  • Run it against the workspace build so the example picks up your local changes:

    npm install
    npm run build
    cd examples/sfReactNative && npm install && npm start
  • Key files:

    • App.tsx: screen switcher that mounts every demo in the app.
    • screens/BasicSignalScreen.tsx: create/read/update a signal with React bindings.
    • screens/ComputedSignalScreen.tsx: derived totals that recalc automatically.
    • screens/EffectsScreen.tsx: createEffect subscriptions with cleanup.
    • screens/PersistentSignalScreen.tsx: AsyncStorage-backed signals for offline-ready state.

Additional resources

  • Playground: signalforge-fogecommunity.vercel.app mirrors the Next.js demo.
  • API Reference: See src/ exports and inline JSDoc; utilities live under signalforge/utils.
  • Benchmarks & bundles: npm run bench and npm run size generate the numbers quoted above. Outputs appear in benchmarks/results.

Performance

Size

  • 2KB gzipped (React entry)
  • 0.5KB core only
  • Zero dependencies

Speed

  • Signal reads: < 1 nanosecond
  • Batch updates: 33x faster than individual
  • Handles 1000s of signals smoothly

Run Benchmarks

npm run benchmark

Try It Live

React Native Demo

git clone https://github.com/forgecommunity/signalforge.git
cd signalforge && npm install && npm run build
cd examples/sfReactNative && npm install && npm start

19 interactive screens showing real-world examples.

Web Demo

signalforge-fogecommunity.vercel.app


Documentation


Get Help

Contributing

git clone https://github.com/forgecommunity/signalforge.git
cd signalforge && npm install && npm run build
npm run test:all

Pull requests welcome!

License

MIT


Built by ForgeCommunity

GitHubnpmDocumentation