An efficient and flexible state management library for building
high-performance, multithreading web applications.
Getting Started · Usage · API Reference · Examples · FAQ
Modern web applications are becoming increasingly complex, pushing the boundaries of what's possible in the browser. Single-threaded JavaScript often struggles to keep up with the demands of sophisticated UIs, real-time interactions, and data-intensive computations — leading to laggy interfaces and compromised user experiences.
While Web Workers (and SharedWorker) offer a path towards parallelism, they introduce challenges around state management, data synchronization, and maintaining coherent application logic across threads.
Coaction was created to bridge this gap — a state management solution that truly embraces the multithreading nature of modern web applications, without sacrificing developer experience.
- Performance first — Offload computationally intensive tasks and state management to worker threads, keeping your UI responsive and fluid.
- Scalable architecture — An intuitive API (inspired by Zustand) with Slices, namespaces, and computed properties promotes modularity and clean code organization.
- Flexible synchronization — Integration with data-transport enables generic transport protocols, supporting various communication patterns including remote synchronization for CRDTs applications.
- Multithreading Sync — Share state between webpage and worker threads. With
data-transport, avoid the complexities of message passing and serialization. - Immutable State with Optional Mutability — Powered by Mutative, providing immutable state transitions with opt-in mutable instances for performance.
- Patch-Based Updates — Efficient incremental state changes through patch-based synchronization, ideal for CRDTs applications.
- Built-in Computed — Derived properties based on state dependencies with automatic caching.
- Slices Pattern — Combine multiple slices into a store with namespace support.
- Extensible Middleware — Enhance store behavior with logging, time-travel debugging, persistence, and more.
- Framework Agnostic — Works with React, Angular, Vue, Svelte, Solid, and state libraries like Redux, Zustand, and MobX.
For React applications:
npm install coaction @coaction/reactFor the core library without any framework:
npm install coactionimport { create } from '@coaction/react';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => state.count++)
}));
const CounterComponent = () => {
const store = useStore();
return (
<div>
<p>Count: {store.count}</p>
<button onClick={store.increment}>Increment</button>
</div>
);
};counter.js
export const counter = (set) => ({
count: 0,
increment: () => set((state) => state.count++)
});worker.js
import { create } from '@coaction/react';
import { counter } from './counter';
create(counter);App.jsx
import { create } from '@coaction/react';
import { counter } from './counter';
const worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
const useStore = create(counter, { worker });
const CounterComponent = () => {
const store = useStore();
return (
<div>
<p>Count in Worker: {store.count}</p>
<button onClick={() => store.increment()}>Increment</button>
</div>
);
};import { create } from '@coaction/react';
const counter = (set, get) => ({
count: 0,
// derived data without cache
get tripleCount() {
return this.count * 3;
},
// derived data with cache
doubleCount: get(
(state) => [state.counter.count],
(count) => count * 2
),
increment() {
set(() => {
// you can use `this` to access the slice state
this.count += 1;
});
}
});
const useStore = create(
{
counter
},
{
sliceMode: 'slices'
}
);Methods that rely on this stay bound when you destructure them from
getState():
const { increment } = useStore.getState().counter;
increment();Coaction operates in two primary modes:
The store is managed entirely within the webpage thread. Patch updates are disabled by default for optimal performance.
The worker thread serves as the primary source of shared state, utilizing transport for synchronization. Webpage threads act as clients, accessing and manipulating the state asynchronously.
In shared mode, the library automatically determines the execution context based on transport parameters, handling synchronization seamlessly. You can easily support multiple tabs, multithreading, or multiprocessing.
For a 3D scene shared across several tabs, you can effortlessly handle state management using Coaction:
coaction-example.mp4
Shared Mode — Sequence Diagram
sequenceDiagram
participant Client as Webpage Thread (Client)
participant Main as Worker Thread (Main)
activate Client
Note over Client: Start Worker Thread
activate Main
Client ->> Main: Trigger fullSync event after startup
activate Main
Main -->> Client: Synchronize data (full state)
deactivate Main
Note over Client: User triggers a UI event
Client ->> Main: Send Store method and parameters
activate Main
Main ->> Main: Execute the corresponding method
Main -->> Client: Synchronize state (patches)
Note over Client: Render new state
Main -->> Client: Asynchronously respond with method execution result
deactivate Main
deactivate Client
Benchmark measuring ops/sec to update 50K arrays and 1K objects — higher is better (source).
Benchmark snapshot from the current
scripts/benchmark.tscomparison
| Library | ops/sec | Relative |
|---|---|---|
| Coaction | 5,272 | 1.0x |
| Coaction with Mutative | 4,626 | 0.88x |
| Zustand | 5,233 | 0.99x |
| Zustand with Immer | 253 | 0.05x |
Coaction performs on par with Zustand in standard usage. The key difference emerges with immutable helpers: Coaction with Mutative is ~18.3x faster than Zustand with Immer (4,626 vs 253 ops/sec), thanks to Mutative's efficient state update mechanism.
Coaction inherits Zustand's intuitive API design while adding built-in support for features Zustand doesn't offer out of the box:
| Feature | Coaction | Zustand |
|---|---|---|
| Built-in multithreading | ✅ | ❌ |
| Getter accessor support | ✅ | ❌ |
| Built-in computed properties | ✅ | ❌ |
| Built-in namespace Slices | ✅ | ❌ |
| Built-in auto selector for state | ✅ | ❌ |
| Built-in multiple stores selector | ✅ | ❌ |
| Easy middleware implementation | ✅ | ❌ |
this support in getter/action |
✅ | ❌ |
Some features may have community solutions in Zustand; Coaction provides a more unified and streamlined API suited for modern web application development.
Regenerate the reference from source with pnpm docs:api.
create() infers store shape from createState by default (sliceMode: 'auto').
For backward compatibility, auto still treats a non-empty object whose
enumerable values are all functions as slices. That shape is ambiguous with a
plain store that only contains methods, so development builds warn and you
should set sliceMode explicitly.
'single'— Treat an object as a single store, even if all values are functions.'slices'— Strict slices mode with validation.
const singleStore = create(
{
ping() {
return 'pong';
}
},
{ sliceMode: 'single' }
);
const slicesStore = create(
{
counter: (set) => ({
count: 0,
increment() {
set((draft) => {
draft.counter.count += 1;
});
}
})
},
{ sliceMode: 'slices' }
);Refactor a general store into a multithreading reusable store — the same source runs on both the webpage and the worker, with isolated references but synchronized state:
store.js
+ const worker = globalThis.SharedWorker
+ ? new SharedWorker(new URL('./store.js', import.meta.url), { type: 'module' })
+ : undefined;
export const store = create(
(set) => ({
count: 0,
increment() {
set((draft) => {
draft.count += 1;
});
}
}),
+ { worker }
);TypeScript note: In the webpage context, the store type is
AsyncStore(methods become asynchronous and are proxied to the worker). In the worker context, it'sStore. See the reusable store example.
Coaction is designed to work with a wide range of libraries and frameworks.
| Framework | Package |
|---|---|
| React | @coaction/react |
| Vue | @coaction/vue |
| Angular | @coaction/ng |
| Svelte | @coaction/svelte |
| Solid | @coaction/solid |
| Yjs | @coaction/yjs |
| Library | Package |
|---|---|
| MobX | @coaction/mobx |
| Pinia | @coaction/pinia |
| Zustand | @coaction/zustand |
| Redux Toolkit | @coaction/redux |
| Jotai | @coaction/jotai |
| XState | @coaction/xstate |
| Valtio | @coaction/valtio |
| alien-signals | @coaction/alien-signals |
Note: Slices mode is a core
coactionfeature. Third-party state adapters only support whole-store binding.
| Middleware | Package |
|---|---|
| Logger | @coaction/logger |
| Persist | @coaction/persist |
| Undo/Redo | @coaction/history |
For production collaboration setups with @coaction/yjs, see:
Can I use Coaction without multithreading?
Absolutely. Coaction supports single-threaded mode with its full API. In default single-threaded mode, it doesn't use patch updates, ensuring optimal performance.
Why is Coaction faster than Zustand with Immer?
Coaction uses Mutative, which provides a faster state update mechanism. Mutative allows mutable instances for performance optimization, whereas Immer's pure immutable approach incurs more overhead.
Why can Coaction integrate with both observable and immutable state libraries?
Coaction is built on Mutative, so it works regardless of whether the state library is immutable or observable. It binds to the existing state object, obtains patches through proxy execution, and applies them to the third-party state library.
Does Coaction support CRDTs?
Yes. Coaction achieves remote synchronization through data-transport, making it well-suited for CRDTs applications. For Yjs-specific synchronization, see the @coaction/yjs documentation.
Does Coaction support multiple tabs?
Yes. State synchronization between multiple tabs is supported via data-transport. Consider using SharedWorker for sharing state across tabs.
Maintainer Guide
packages/core— runtime creation, authority model, patch flow, transport integration, middleware hooks, adapter hookspackages/coaction-*framework bindings — React, Vue, Angular, Svelte, Solid wrappers around core storespackages/coaction-*state adapters — whole-store integrations for external runtimes such as Zustand, MobX, Pinia, Redux, Jotai, Valtio, and XStatepackages/coaction-*middlewares — logger, persist, history, yjsexamples/*— runnable integration and end-to-end examplesdocs/architecture/*— maintainer-oriented runtime, support, and API evolution docs
| Surface | Official contract |
|---|---|
| Native Coaction stores | Local and shared single/slices stores are supported. |
| Binder-backed adapters | Whole-store only. Shared main/client is currently maintained for MobX, Pinia, and Zustand. |
| Middleware authority | Logger is supported on local/main and limited on clients. Persist and history belong on the authority store. |
| Yjs | Local/main store binding is supported. Client mode is unsupported. |
For the package-by-package status and boundary notes, see the full support matrix.
- Core runtime and type coverage lives in
packages/core/test. - Shared binder adapter coverage lives in
packages/*/test/contract.test.ts. - Package-specific behavior and branch coverage lives in each package
test/directory. - Integration and end-to-end coverage lives in
packages/coaction-yjs/test/ws.integration.test.tsandexamples/e2e/test.
- Read the adapter contract first.
- Follow the adapter contribution guide.
- Add the shared binder contract suite when the package is binder-backed.
- Update the support matrix in the same change as any new guarantee.
- Concept inspired by Partytown
- API design inspired by Zustand
- Technical reference: React + Redux + Comlink = Off-main-thread
Coaction is MIT licensed.

