synstate
v0.1.2
Published
Type-safe State Management Library for TypeScript/JavaScript
Readme
SynState
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
createStateandcreateReducerfor global state - 📡 Event System: Built-in
createValueEmitter,createEventEmitterfor 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 synstateOr with other package managers:
# Yarn
yarn add synstate
# pnpm
pnpm add synstateQuick 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-hooksimport 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/settercreateReducer: Create state by reducer and initial valuecreateBooleanState: Specialized state for boolean values
Event System
Built-in event emitter for event-driven patterns:
createValueEmitter: Create type-safe event emitterscreateEventEmitter: 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 sourceof(value): Create observable from a single valuefromArray(array): Create observable from arrayfromPromise(promise): Create observable from promiseinterval(ms): Emit values at intervalstimer(delay): Emit after delay
Operators
filter(predicate): Filter valuesmap(fn): Transform valuesscan(reducer, seed): Accumulate valuesdebounceTime(ms): Debounce emissionsthrottleTime(ms): Throttle emissionsskipIfNoChange(): Skip duplicate valuestakeUntil(notifier): Complete on notifier emission
Combination
combine(observables): Combine latest values from multiple sourcesmerge(observables): Merge multiple streamszip(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
InitializedObservablewhich always holds an initial value, making it ideal for representing state - Simpler API: Most use cases are covered by
createState,createReducer, andcreateEventEmitter - 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.
