signalforge
v1.0.1
Published
Fine-grained reactive state management with automatic dependency tracking - Ultra-optimized, zero dependencies
Maintainers
Readme
SignalForge

Fine-Grained Reactive State Management for Modern JavaScript
Quick Start • Examples • Docs • API 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()); // 10Why SignalForge?
Easy to Learn
Only 3 functions to master:
createSignal(value) // Store data
createComputed(() => ...) // Auto-calculate from other signals
createEffect(() => ...) // Run code when signals changeWorks 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 signalforge2. 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: 6Computed - 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 restartReact 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 onlyUsage
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 iosIncludes: 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 devKey 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 startKey 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:createEffectsubscriptions with cleanup.screens/PersistentSignalScreen.tsx: AsyncStorage-backed signals for offline-ready state.
Additional resources
- Playground:
signalforge-fogecommunity.vercel.appmirrors the Next.js demo. - API Reference: See
src/exports and inline JSDoc; utilities live undersignalforge/utils. - Benchmarks & bundles:
npm run benchandnpm run sizegenerate the numbers quoted above. Outputs appear inbenchmarks/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 benchmarkTry 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 start19 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:allPull requests welcome!
License
MIT
Built by ForgeCommunity
GitHub • npm • Documentation
