# Homeostate documentation > State-manager and CRDT-backend agnostic sync engine: keep a Zustand, Redux, MobX, Jotai, Valtio, TanStack Store or MobX-State-Tree store in sync with a Yjs, Loro or Automerge document. --- url: https://homeostate.pages.dev/docs/getting-started.md --- # Getting started Homeostate keeps a store you already use in sync with a CRDT document, so every peer that shares the document shares the state. You pick three packages: 1. [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md), the sync engine. 2. One store adapter for your state manager: [Zustand](https://homeostate.pages.dev/docs/store-zustand/introduction.md), [Redux](https://homeostate.pages.dev/docs/store-redux/introduction.md), [MobX](https://homeostate.pages.dev/docs/store-mobx/introduction.md), [MobX-State-Tree](https://homeostate.pages.dev/docs/store-mobx-state-tree/introduction.md), [Jotai](https://homeostate.pages.dev/docs/store-jotai/introduction.md), [Valtio](https://homeostate.pages.dev/docs/store-valtio/introduction.md) or [TanStack Store](https://homeostate.pages.dev/docs/store-tanstack/introduction.md). 3. One CRDT backend for your replication library: [Yjs](https://homeostate.pages.dev/docs/crdt-yjs/introduction.md), [Loro](https://homeostate.pages.dev/docs/crdt-loro/introduction.md) or [Automerge](https://homeostate.pages.dev/docs/crdt-automerge/introduction.md). > [!WARNING] > Every package is in `0.x`. The API is still being designed and may change in any minor > version. ## Install This guide uses Zustand and Yjs: ```bash install npm install @homeostate/core @homeostate/store-zustand zustand @homeostate/crdt-yjs yjs ``` ## Sync a store Wrap the state creator with the `homeostate` middleware and give it a backend. The store connects as soon as it is created: ```ts title="counter.ts" import * as Y from "yjs"; import { create } from "zustand"; import { createYjsBackend } from "@homeostate/crdt-yjs"; import { homeostate } from "@homeostate/store-zustand"; type CounterState = { count: number; increment: () => void }; export const doc = new Y.Doc(); export const useCounter = create()( homeostate(createYjsBackend(doc, "shared"), (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })), ); ``` `count` now lives in `doc.getMap("shared")` as well as in the store. `increment` is a function, so the default filter keeps it out of the document. Every other adapter works through `createSyncEngine` from core instead of a middleware; each package's introduction shows its setup. ## Connect peers The backend only holds the synced state. Replicating the document between peers is the CRDT library's job, with any transport it supports. With Yjs, a [y-websocket](https://github.com/yjs/y-websocket) provider is enough: ```ts import { WebsocketProvider } from "y-websocket"; import { doc } from "./counter"; new WebsocketProvider("wss://sync.example.com", "counter-room", doc); ``` Open the app in two tabs and `increment` in one updates `count` in the other. ## Next steps - [Concepts](https://homeostate.pages.dev/docs/concepts.md) explains the two contracts the packages implement and how `connect()` reconciles a store with a document that already has state. - [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md) covers the engine options. --- url: https://homeostate.pages.dev/docs/concepts.md --- # Concepts Homeostate is one engine and two contracts. The engine, [`createSyncEngine`](https://homeostate.pages.dev/docs/core/introduction.md), sits between a store and a replicated document and moves plain JSON between them. It knows nothing about any particular state manager or CRDT library: `store-*` packages implement `StoreAdapter` for a state manager, and `crdt-*` packages implement `CrdtBackend` for a CRDT library. ```ts import { createSyncEngine } from "@homeostate/core"; const engine = createSyncEngine(backend, adapter, { seed: "if-empty" }); engine.connect(); engine.isConnected(); // true engine.disconnect(); ``` ## Store adapters A `StoreAdapter` reads, replaces and watches the store: ```ts interface StoreAdapter { getState: () => S; setState: (state: S) => void; subscribe: (onStoreChange: () => void) => Unsubscribe; } ``` `setState` is called only for changes that come from the backend. The engine ignores store notifications raised while it runs, so an adapter needs no echo suppression of its own. Synced state must be plain JSON: objects, arrays, strings, numbers, booleans and `null`. Functions are dropped by the default filter. Other values such as `Date`, `Map` or `Set` are neither diffed nor synced. ## CRDT backends A `CrdtBackend` holds the synced part of the state in a CRDT or any other replicated store: ```ts interface CrdtBackend { read: () => unknown; write: (next: unknown) => void; subscribe: (onRemoteChange: () => void) => Unsubscribe; } ``` - `read` returns a plain JSON snapshot that does not alias the backend's internals. - `write` makes the backend equal to `next` in one atomic transaction. The backend decides how fine-grained the operations are; core exports `getChanges` so a backend can turn a `write` into small edits instead of replacing the document. - `subscribe` reports changes that did not come through the backend's own `write`: imports from peers and local edits made directly on the document. `createMemoryBackend()` from core is a plain JSON backend without replication, meant for tests. ## Choosing what to sync The `filter` option decides which top-level keys are synced, in both directions: ```ts createSyncEngine(backend, adapter, { filter: (key, value) => typeof value !== "function" && key !== "draft", }); ``` The default, `defaultSyncFilter`, excludes functions. Keys the filter excludes are never read into the store from the backend and never written out, so they stay local to each peer. ## Connecting `connect()` reconciles the store and the backend per key, over the keys the filter allows: - A key the backend holds wins over the store's value, so a peer joining a room that already has state adopts it instead of overwriting it. - A synced key the backend does not hold stays in the store. With the default `seed: "if-empty"` those keys are written into the backend in one write. With `seed: "never"` they are left for the next local change. After that, the backend owns the synced document. Each local change replaces it with the store's filtered state, and a key removed from the backend is removed from the store. > [!IMPORTANT] > A reconnect adopts the backend again, so edits made while disconnected are dropped unless > they live in keys the filter keeps local. --- url: https://homeostate.pages.dev/docs/about.md --- # About Homeostate keeps a store you already use in sync with a CRDT document. It is one sync engine, [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md), plus a store adapter per state manager and a backend per CRDT library, each published as its own package. - **Source:** [github.com/mixedrays/homeostate](https://github.com/mixedrays/homeostate), a pnpm workspace with every package, the playground and the benchmarks. Issues and pull requests go there. - **Packages:** [npmjs.com/org/homeostate](https://www.npmjs.com/org/homeostate). Each package is versioned and released on its own; its changelog is the last page of its section. - **License:** MIT. ## These docs Every page is a markdown file in the repository: package pages live in each package's `docs/` folder, next to the code, and guides like this one in `apps/docs/content/`. Add `.md` to any page's URL for its plain markdown. [llms.txt](https://homeostate.pages.dev/llms.txt) indexes every page, and [llms-full.txt](https://homeostate.pages.dev/llms-full.txt) has all of them in one file. --- url: https://homeostate.pages.dev/docs/core/introduction.md --- # @homeostate/core > [!WARNING] > **Draft / placeholder release.** `0.0.0` reserves the name while the API is still > being designed. Nothing here is stable — do not depend on it yet. State-manager and CRDT-backend agnostic sync engine. It keeps a store, reached through a `StoreAdapter`, in sync with a `CrdtBackend` such as [`@homeostate/crdt-yjs`](https://homeostate.pages.dev/docs/crdt-yjs/introduction.md), [`@homeostate/crdt-loro`](https://homeostate.pages.dev/docs/crdt-loro/introduction.md), or [`@homeostate/crdt-automerge`](https://homeostate.pages.dev/docs/crdt-automerge/introduction.md). ## Install ```bash install npm install @homeostate/core @homeostate/crdt-yjs yjs ``` `@homeostate/core` has no runtime dependencies; pick a backend package for the CRDT library you use. ## Usage ```ts import { createSyncEngine } from "@homeostate/core"; import { createYjsBackend } from "@homeostate/crdt-yjs"; const engine = createSyncEngine(createYjsBackend(doc, "shared"), adapter, { seed: "if-empty", }); engine.connect(); ``` `createMemoryBackend()` is a plain-JSON backend without replication, meant for tests. `getChanges` is exported so a backend can turn a `write(next)` into fine-grained operations. ## Connecting `connect()` reconciles the store and the backend per key, over the view the `filter` allows: - A key the backend holds wins over the store's value, so a peer joining a populated room adopts it rather than overwriting it. - A synced key the backend does not hold stays in the store. With the default `seed: 'if-empty'` those keys are written into the backend in one write; with `seed: 'never'` they are left to the next local change. - Keys the `filter` excludes are never read into the store and never written out. Afterwards the backend owns the synced document: each local change replaces it with the store's filtered state, and a key removed from the backend is removed from the store. A reconnect adopts the backend again, so edits made while disconnected are dropped unless they are still in the store's local-only keys. See the [repository](https://github.com/mixedrays/homeostate) for the full workspace, store adapters, and a runnable playground. --- url: https://homeostate.pages.dev/docs/core/changelog.md --- # @homeostate/core ## 0.1.1 ### Patch Changes - 8bd452e: Init release --- url: https://homeostate.pages.dev/docs/crdt-yjs/introduction.md --- # @homeostate/crdt-yjs > [!WARNING] > **Draft / placeholder release.** `0.0.0` reserves the name while the API is still > being designed. Nothing here is stable — do not depend on it yet. [Yjs](https://github.com/yjs/yjs) backend for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It maps the synced state onto a `Y.Map` (nested objects become `Y.Map`, arrays `Y.Array`, strings `Y.Text`) and writes fine-grained operations derived from `getChanges`. ## Install ```bash install npm install @homeostate/core @homeostate/crdt-yjs yjs ``` `yjs` is a peer dependency. ## Usage ```ts import * as Y from "yjs"; import { createSyncEngine } from "@homeostate/core"; import { createYjsBackend } from "@homeostate/crdt-yjs"; const doc = new Y.Doc(); const engine = createSyncEngine(createYjsBackend(doc, "shared"), adapter); engine.connect(); ``` The synced state lives in `doc.getMap('shared')`; a middle-of-array delete, a toggle, or a keystroke each produce one small update. --- url: https://homeostate.pages.dev/docs/crdt-yjs/changelog.md --- # @homeostate/crdt-yjs ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1 --- url: https://homeostate.pages.dev/docs/crdt-loro/introduction.md --- # @homeostate/crdt-loro > [!WARNING] > **Draft / placeholder release.** `0.0.0` reserves the name while the API is still > being designed. Nothing here is stable — do not depend on it yet. [Loro](https://github.com/loro-dev/loro) backend for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It maps the synced state onto a `LoroMap` (nested objects become `LoroMap`, arrays `LoroList`, strings `LoroText`) and writes fine-grained operations derived from `getChanges`, committed as one transaction per `write`. ## Install ```bash install npm install @homeostate/core @homeostate/crdt-loro loro-crdt ``` `loro-crdt` is a peer dependency. ## Usage ```ts import { LoroDoc } from "loro-crdt"; import { createSyncEngine } from "@homeostate/core"; import { createLoroBackend } from "@homeostate/crdt-loro"; const doc = new LoroDoc(); const engine = createSyncEngine(createLoroBackend(doc, "shared"), adapter); engine.connect(); ``` The synced state lives in `doc.getMap('shared')`; a middle-of-array delete, a toggle, or a keystroke each produce one small update. Replication is yours to wire, for example with `doc.subscribeLocalUpdates` on one side and `doc.import` on the other. The engine hears about every commit that did not come through its own `write`, imports and local edits alike. Loro stores `undefined` as `null`, so such values read back as `null`. --- url: https://homeostate.pages.dev/docs/crdt-loro/changelog.md --- # @homeostate/crdt-loro ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1 --- url: https://homeostate.pages.dev/docs/crdt-automerge/introduction.md --- # @homeostate/crdt-automerge > [!WARNING] > **Draft / placeholder release.** `0.0.0` reserves the name while the API is still > being designed. Nothing here is stable — do not depend on it yet. [Automerge](https://automerge.org) backend for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It maps the synced state onto one key of an Automerge document (nested objects become maps, arrays lists, strings text) and writes fine-grained operations derived from `getChanges`, one Automerge change per `write`. ## Install ```bash install npm install @homeostate/core @homeostate/crdt-automerge @automerge/automerge ``` `@automerge/automerge` 3.x is a peer dependency. ## Usage ```ts import * as A from "@automerge/automerge"; import { createSyncEngine } from "@homeostate/core"; import { createAutomergeBackend, createAutomergeHandle, } from "@homeostate/crdt-automerge"; const handle = createAutomergeHandle(A.init()); const engine = createSyncEngine( createAutomergeBackend(handle, "shared"), adapter, ); engine.connect(); ``` Automerge documents are immutable values: every change returns a new document and leaves the old one behind. `createAutomergeHandle` holds the current document so the backend and your replication code share it: - `handle.doc()` returns the current document. - `handle.change(fn)` applies a local change through `A.change`. - `handle.update((doc) => ...)` replaces the document with the result of `A.merge`, `A.applyChanges`, `A.loadIncremental`, `A.receiveSyncMessage`, or any other Automerge call. - `handle.subscribe(({ doc, local }) => ...)` fires whenever the heads change; `local` is true for `change` and false for `update`. The synced state lives in `handle.doc().shared`; a middle-of-array delete, a toggle, or a keystroke each produce one small change. Replication is yours to wire, for example: ```ts handle.subscribe(({ doc, local }) => { if (local) send(A.getLastLocalChange(doc)); }); onMessage((change) => handle.update((doc) => A.applyChanges(doc, [change])[0])); ``` The engine hears about every change that did not come through its own `write`, imports and local edits alike. `read()` returns plain JSON. Automerge keeps every object that the last change did not touch, so the backend caches copies per source object and a read after a toggle copies only the containers on the path to the toggled item. Snapshots are shared between reads; treat them as immutable. `undefined` is not JSON and Automerge rejects it, so the backend drops object entries whose value is `undefined` and stores `undefined` array items as `null`, as `JSON.stringify` does. `@automerge/automerge` ships WebAssembly. Node loads it directly; for bundlers see [Automerge's documentation](https://automerge.org/docs/), or import `@automerge/automerge/slim` and initialize the module yourself. --- url: https://homeostate.pages.dev/docs/crdt-automerge/changelog.md --- # @homeostate/crdt-automerge ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1 --- url: https://homeostate.pages.dev/docs/store-zustand/introduction.md --- # @homeostate/store-zustand > [!WARNING] > **Early release.** The API is still being designed and may change in any `0.x` minor > version. [Zustand](https://github.com/pmndrs/zustand) store adapter and middleware for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It keeps a Zustand store in sync with a CRDT backend such as [`@homeostate/crdt-yjs`](https://homeostate.pages.dev/docs/crdt-yjs/introduction.md), [`@homeostate/crdt-loro`](https://homeostate.pages.dev/docs/crdt-loro/introduction.md), or [`@homeostate/crdt-automerge`](https://homeostate.pages.dev/docs/crdt-automerge/introduction.md). ## Install ```bash install npm install @homeostate/core @homeostate/store-zustand zustand @homeostate/crdt-yjs yjs ``` `zustand` 4.5 or 5 is a peer dependency. ## Usage Wrap the state creator with the `homeostate` middleware. The store connects as soon as it is created, and the engine is exposed as `store.homeostate`: ```ts import * as Y from "yjs"; import { create } from "zustand"; import { createYjsBackend } from "@homeostate/crdt-yjs"; import { homeostate } from "@homeostate/store-zustand"; type CounterState = { count: number; increment: () => void }; const doc = new Y.Doc(); const useCounter = create()( homeostate(createYjsBackend(doc, "shared"), (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })), ); useCounter.homeostate.disconnect(); // stop syncing ``` The optional third argument is the engine's `SyncEngineConfig` (`filter`, `seed`). To sync a store you already have, create the engine yourself with `createZustandAdapter`: ```ts import { createSyncEngine } from "@homeostate/core"; import { createZustandAdapter } from "@homeostate/store-zustand"; const engine = createSyncEngine( createYjsBackend(doc, "shared"), createZustandAdapter(useCounter), ); engine.connect(); ``` Actions and other functions in the state are skipped by the default sync filter, so only data is synced. Remote changes replace the state with `setState(state, true)`. --- url: https://homeostate.pages.dev/docs/store-zustand/changelog.md --- # @homeostate/store-zustand ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1 --- url: https://homeostate.pages.dev/docs/store-redux/introduction.md --- # @homeostate/store-redux > [!WARNING] > **Early release.** The API is still being designed and may change in any `0.x` minor > version. [Redux](https://redux.js.org) store adapter for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It keeps a Redux store in sync with a CRDT backend such as [`@homeostate/crdt-yjs`](https://homeostate.pages.dev/docs/crdt-yjs/introduction.md), [`@homeostate/crdt-loro`](https://homeostate.pages.dev/docs/crdt-loro/introduction.md), or [`@homeostate/crdt-automerge`](https://homeostate.pages.dev/docs/crdt-automerge/introduction.md). ## Install ```bash install npm install @homeostate/core @homeostate/store-redux @reduxjs/toolkit @homeostate/crdt-yjs yjs ``` `redux` 5 is a peer dependency; Redux Toolkit 2 already depends on it. ## Usage The adapter applies remote changes by dispatching an action that replaces the whole state, so add a reducer for it and pass its action creator: ```ts import * as Y from "yjs"; import { configureStore, createSlice, type PayloadAction, } from "@reduxjs/toolkit"; import { createSyncEngine } from "@homeostate/core"; import { createYjsBackend } from "@homeostate/crdt-yjs"; import { createReduxAdapter } from "@homeostate/store-redux"; type CounterState = { count: number }; const counter = createSlice({ name: "counter", initialState: { count: 0 } as CounterState, reducers: { increment: (state) => { state.count++; }, replaceState: (_state, action: PayloadAction) => action.payload, }, }); const store = configureStore({ reducer: counter.reducer }); const engine = createSyncEngine( createYjsBackend(new Y.Doc(), "shared"), createReduxAdapter(store, counter.actions.replaceState), ); engine.connect(); ``` Immer-frozen state is fine: the engine never mutates store state in place. Local changes are sent only when a dispatch produces a new state object. --- url: https://homeostate.pages.dev/docs/store-redux/changelog.md --- # @homeostate/store-redux ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1 --- url: https://homeostate.pages.dev/docs/store-mobx/introduction.md --- # @homeostate/store-mobx > [!WARNING] > **Early release.** The API is still being designed and may change in any `0.x` minor > version. [MobX](https://mobx.js.org) store adapter for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It keeps chosen properties of an observable store in sync with a CRDT backend such as [`@homeostate/crdt-yjs`](https://homeostate.pages.dev/docs/crdt-yjs/introduction.md), [`@homeostate/crdt-loro`](https://homeostate.pages.dev/docs/crdt-loro/introduction.md), or [`@homeostate/crdt-automerge`](https://homeostate.pages.dev/docs/crdt-automerge/introduction.md). ## Install ```bash install npm install @homeostate/core @homeostate/store-mobx mobx @homeostate/crdt-yjs yjs ``` `mobx` 6 or 7 is a peer dependency. ## Usage Pass the store and the keys to sync. Everything else on the store stays local: ```ts import * as Y from "yjs"; import { makeAutoObservable } from "mobx"; import { createSyncEngine } from "@homeostate/core"; import { createYjsBackend } from "@homeostate/crdt-yjs"; import { createMobxAdapter } from "@homeostate/store-mobx"; class TodoStore { todos: { id: string; title: string; done: boolean }[] = []; filter = "all"; constructor() { makeAutoObservable(this); } add(title: string) { this.todos.push({ id: crypto.randomUUID(), title, done: false }); } } const store = new TodoStore(); const engine = createSyncEngine( createYjsBackend(new Y.Doc(), "shared"), createMobxAdapter(store, ["todos", "filter"]), ); engine.connect(); ``` A remote change is reconciled into the observable tree instead of being assigned over it: only the fields, array elements and keys that differ are written, inside one action. Items that did not change keep their identity, so `observer` components, reactions and effects that depend on them stay quiet. --- url: https://homeostate.pages.dev/docs/store-mobx/changelog.md --- # @homeostate/store-mobx ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1 --- url: https://homeostate.pages.dev/docs/store-mobx-state-tree/introduction.md --- # @homeostate/store-mobx-state-tree > [!WARNING] > **Early release.** The API is still being designed and may change in any `0.x` minor > version. [MobX-State-Tree](https://mobx-state-tree.js.org) store adapter for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It keeps a state tree node in sync with a CRDT backend such as [`@homeostate/crdt-yjs`](https://homeostate.pages.dev/docs/crdt-yjs/introduction.md), [`@homeostate/crdt-loro`](https://homeostate.pages.dev/docs/crdt-loro/introduction.md), or [`@homeostate/crdt-automerge`](https://homeostate.pages.dev/docs/crdt-automerge/introduction.md). ## Install ```bash install npm install @homeostate/core @homeostate/store-mobx-state-tree mobx mobx-state-tree @homeostate/crdt-yjs yjs ``` `mobx` 6 or 7 and `mobx-state-tree` 7 or 8 are peer dependencies. ## Usage ```ts import * as Y from "yjs"; import { types } from "mobx-state-tree"; import { createSyncEngine } from "@homeostate/core"; import { createYjsBackend } from "@homeostate/crdt-yjs"; import { createMobxStateTreeAdapter } from "@homeostate/store-mobx-state-tree"; const Todo = types.model({ id: types.identifier, title: types.string, done: false, }); const TodoStore = types.model({ todos: types.array(Todo) }).actions((self) => ({ add(title: string) { self.todos.push({ id: crypto.randomUUID(), title }); }, })); const store = TodoStore.create(); const engine = createSyncEngine( createYjsBackend(new Y.Doc(), "shared"), createMobxStateTreeAdapter(store), ); engine.connect(); ``` The adapter syncs the node's snapshot: local changes go to the backend once per action, and remote changes are applied with `applySnapshot`, so instances with an identifier are reconciled in place instead of being recreated. Pass the root instance, or any subtree node to sync only that part. --- url: https://homeostate.pages.dev/docs/store-mobx-state-tree/changelog.md --- # @homeostate/store-mobx-state-tree ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1 --- url: https://homeostate.pages.dev/docs/store-jotai/introduction.md --- # @homeostate/store-jotai > [!WARNING] > **Early release.** The API is still being designed and may change in any `0.x` minor > version. [Jotai](https://jotai.org) store adapter for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It keeps a writable atom in sync with a CRDT backend such as [`@homeostate/crdt-yjs`](https://homeostate.pages.dev/docs/crdt-yjs/introduction.md), [`@homeostate/crdt-loro`](https://homeostate.pages.dev/docs/crdt-loro/introduction.md), or [`@homeostate/crdt-automerge`](https://homeostate.pages.dev/docs/crdt-automerge/introduction.md). ## Install ```bash install npm install @homeostate/core @homeostate/store-jotai jotai @homeostate/crdt-yjs yjs ``` `jotai` 2 or 3 is a peer dependency. ## Usage The adapter syncs one writable atom that holds the whole synced state and accepts a full replacement: ```ts import * as Y from "yjs"; import { atom, createStore } from "jotai"; import { createSyncEngine } from "@homeostate/core"; import { createYjsBackend } from "@homeostate/crdt-yjs"; import { createJotaiAdapter } from "@homeostate/store-jotai"; const counterAtom = atom({ count: 0 }); const store = createStore(); const engine = createSyncEngine( createYjsBackend(new Y.Doc(), "shared"), createJotaiAdapter(counterAtom, store), ); engine.connect(); ``` The store argument is optional and defaults to Jotai's default store. Pass the same store you give `` if you use one. To sync state spread across several atoms, pass a derived writable atom that reads them and fans a replacement out to each: ```ts type Todo = { id: string; title: string }; const todosAtom = atom([]); const filterAtom = atom("all"); const syncedAtom = atom( (get) => ({ todos: get(todosAtom), filter: get(filterAtom) }), (_get, set, next: { todos: Todo[]; filter: string }) => { set(todosAtom, next.todos); set(filterAtom, next.filter); }, ); createJotaiAdapter(syncedAtom, store); ``` --- url: https://homeostate.pages.dev/docs/store-jotai/changelog.md --- # @homeostate/store-jotai ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1 --- url: https://homeostate.pages.dev/docs/store-valtio/introduction.md --- # @homeostate/store-valtio > [!WARNING] > **Early release.** The API is still being designed and may change in any `0.x` minor > version. [Valtio](https://valtio.dev) store adapter for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It keeps a Valtio proxy in sync with a CRDT backend such as [`@homeostate/crdt-yjs`](https://homeostate.pages.dev/docs/crdt-yjs/introduction.md), [`@homeostate/crdt-loro`](https://homeostate.pages.dev/docs/crdt-loro/introduction.md), or [`@homeostate/crdt-automerge`](https://homeostate.pages.dev/docs/crdt-automerge/introduction.md). ## Install ```bash install npm install @homeostate/core @homeostate/store-valtio valtio @homeostate/crdt-yjs yjs ``` `valtio` 2 is a peer dependency. ## Usage ```ts import * as Y from "yjs"; import { proxy } from "valtio"; import { createSyncEngine } from "@homeostate/core"; import { createYjsBackend } from "@homeostate/crdt-yjs"; import { createValtioAdapter } from "@homeostate/store-valtio"; const state = proxy({ todos: [] as { id: string; title: string; done: boolean }[], filter: "all", }); const engine = createSyncEngine( createYjsBackend(new Y.Doc(), "shared"), createValtioAdapter(state), ); engine.connect(); state.todos.push({ id: crypto.randomUUID(), title: "Write docs", done: false }); ``` Mutate the proxy as usual; every change is sent to the backend. Remote changes mutate only the paths that differ, so unchanged subtrees keep their proxy identity and components reading them through `useSnapshot` do not re-render. --- url: https://homeostate.pages.dev/docs/store-valtio/changelog.md --- # @homeostate/store-valtio ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1 --- url: https://homeostate.pages.dev/docs/store-tanstack/introduction.md --- # @homeostate/store-tanstack > [!WARNING] > **Early release.** The API is still being designed and may change in any `0.x` minor > version. [TanStack Store](https://tanstack.com/store) adapter for [`@homeostate/core`](https://homeostate.pages.dev/docs/core/introduction.md). It keeps a TanStack `Store` in sync with a CRDT backend such as [`@homeostate/crdt-yjs`](https://homeostate.pages.dev/docs/crdt-yjs/introduction.md), [`@homeostate/crdt-loro`](https://homeostate.pages.dev/docs/crdt-loro/introduction.md), or [`@homeostate/crdt-automerge`](https://homeostate.pages.dev/docs/crdt-automerge/introduction.md). ## Install ```bash install npm install @homeostate/core @homeostate/store-tanstack @tanstack/store @homeostate/crdt-yjs yjs ``` `@tanstack/store` 0.11 is a peer dependency. ## Usage ```ts import * as Y from "yjs"; import { createStore } from "@tanstack/store"; import { createSyncEngine } from "@homeostate/core"; import { createYjsBackend } from "@homeostate/crdt-yjs"; import { createTanStackStoreAdapter } from "@homeostate/store-tanstack"; const store = createStore({ count: 0 }); const engine = createSyncEngine( createYjsBackend(new Y.Doc(), "shared"), createTanStackStoreAdapter(store), ); engine.connect(); store.setState((state) => ({ ...state, count: state.count + 1 })); ``` Stores created with or without an actions factory both work; only the state is synced. --- url: https://homeostate.pages.dev/docs/store-tanstack/changelog.md --- # @homeostate/store-tanstack ## 0.1.1 ### Patch Changes - 8bd452e: Init release - Updated dependencies [8bd452e] - @homeostate/core@0.1.1