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.
data-transport, avoid the complexities of message passing and serialization.For React applications:
npm install coaction @coaction/react
For the core library without any framework:
npm install coaction
import { 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:
https://github.com/user-attachments/assets/9eb9f4f8-8d47-433a-8eb2-85f044d6d8fa
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).
Coaction v0.1.5 vs Zustand v5.0.2
| 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.
sliceMode)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:
Absolutely. Coaction supports single-threaded mode with its full API. In default single-threaded mode, it doesn't use patch updates, ensuring optimal performance.
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.
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.
Yes. Coaction achieves remote synchronization through data-transport, making it well-suited for CRDTs applications. For Yjs-specific synchronization, see the @coaction/yjs documentation.
Yes. State synchronization between multiple tabs is supported via data-transport. Consider using SharedWorker for sharing state across tabs.
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.
packages/core/test.packages/*/test/contract.test.ts.test/ directory.packages/coaction-yjs/test/ws.integration.test.ts and examples/e2e/test.Coaction is MIT licensed.