Skip to content

Introduction

SynState Logo

SynState is a lightweight, glitch-free state management library for TypeScript/JavaScript. Simple global state that scales to complex async pipelines — with built-in debounce, switchMap, and more. Fully compatible with React Compiler.

“SynState” is named after “Synchronized + State.” Derived values are always kept in sync through glitch-free propagation — no inconsistent intermediate states. (Details: How SynState Solved the Glitch)

  • 🎯 Simple State Management: Easy-to-use createState and createReducer similar to React useState/useReducer, but for global state.
  • 🔧 Built-in Async Operators: debounce, throttle, switchMap, and more — handle complex async flows in a unified, declarative API without external libraries.
  • ⚡ Glitch-Free Through Every Operator: Every update is one depth-ordered pass over what it reaches, the ones debounce or switchMap start later included, so no subscriber sees a half-updated graph, and an update costs in proportion to the nodes it reaches rather than the paths between them. Operators on Signals shows what an operator written as an effect loses.
  • 🏎️ Very Fast: In the benchmark summary, SynState stays within 2.7× of the fastest library in every scenario: the only library measured in all of them whose worst case stays in single digits. See each library’s worst case.
  • 🚀 Lightweight: ~7.7 kB min+gzip with two external runtime dependencies (ts-data-forge and ts-std-forge, and the first depends on the second). Compare it with other libraries.
  • ⚛️ React-Optimized: Fully compatible with React Compiler. synstate-react-hooks lets you add global state to any component in just a few lines.
  • 🌐 Framework Agnostic: Core library works as-is with vanilla JavaScript, Vue, Svelte, or any framework. For React and Preact, hooks wrappers (synstate-react-hooks / synstate-preact-hooks) are available. For Preact, a Preact Signals integration (synstate-preact-signals) is also provided for fine-grained DOM updates without component re-renders. Without any framework, synstate-dom binds Observables directly to the DOM.

With a single call to createState, you can add global state to your application:

import { createState } from 'synstate';
// Create a reactive state
const [state, setState] = createState(0);
// Subscribe to changes
state.subscribe((count) => {
console.log(count); // 0, 1
});
// Update state
setState(1);

createState creates a reactive state (InitializedObservable) and a setter function. Subscribers are called immediately with the initial value, and again whenever the state is updated.

createState also returns a third element with additional utilities:

const [
state,
setState,
{ updateState, resetState, getSnapshot, initialState },
] = createState(0);
updateState((prev) => prev + 2); // update using a function of the previous value
getSnapshot(); // read current value synchronously (2)
initialState; // the initial value passed to createState (0)
resetState(); // reset to initial value (0)
getSnapshot(); // read current value synchronously (0)

SynState’s power comes from composing Observables. Use pipe to derive new values and derive to compute one value from several sources:

import { createState, derive, type InitializedObservable, map } from 'synstate';
const [count, setCount] = createState<number>(0);
// Read the current value
console.log(count.getSnapshot().value); // 0
// Derive new Observables using pipe
const doubled: InitializedObservable<number> = count.pipe(map((n) => n * 2));
// Derive one value from several Observables
const combined: InitializedObservable<string> = derive(
[count, doubled],
(c, d) => `(${c}, ${d})`,
);
// Subscribe to changes
count.subscribe((value) => {
console.log('count:', value); // 0, 1, 2, 3
});
doubled.subscribe((value) => {
console.log('doubled:', value); // 0, 2, 4, 6
});
combined.subscribe((value) => {
console.log(value); // "(0, 0)", "(1, 2)", "(2, 4)", "(3, 6)"
});
// Update state
setCount(1);
setCount(2);
setCount(3);

For a deeper understanding of how this declarative model works — and why it is better than managing state imperatively — see Declarative State Management.

createState must be called at module scope (outside of components). SynState provides synstate-react-hooks, a companion package that bridges Observables and React:

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: 'alice@example.com',
});
}}
>
{'Set User'}
</button>
</div>
);
};

For more details, see React Integration.