Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-history-and-react-selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@codebelt/classy-store': patch
---

Restore mutable nested state after undo/redo while preserving reactive collection methods and keeping recorded snapshots unchanged. Refresh React selections when selectors or equality functions change, even when the store has not mutated. Update the library's development dependencies and add regression coverage for both fixes.
9 changes: 9 additions & 0 deletions .changeset/next-app-router-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@codebelt/classy-store': minor
---

Add a server-safe `@codebelt/classy-store/react/server` entry for `prepareStoreForHydration`, and mark the published React hooks entry with `use client`. The existing preparation export remains available from `/react`; both use the same capture cache within a module graph.

Fix `Snapshot<T>` function classification so methods with typed parameters retain their callable signatures under strict TypeScript, including reactive collection queries, overloads, and generic functions.

Add packed-package production Next.js App Router tests with React Compiler off/on for package boundaries, request isolation, controlled inputs, streaming hydration, navigation, refresh, and cleanup, plus CI coverage and integration documentation.
7 changes: 7 additions & 0 deletions .changeset/persist-operation-ordering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@codebelt/classy-store': patch
---

Keep asynchronous persistence writes and removals in request order. Reads wait for prior writes and ignore results superseded by a newer read, save, clear, or local edit. Clearing storage cancels pending auto-saves, and cleanup skips queued operations that have not started.

Ignore outdated browser storage events so two-tab synchronization cannot overwrite a newer local save with an older event. Add deterministic async regressions and real two-tab React tests in Chromium, Firefox, and WebKit.
7 changes: 7 additions & 0 deletions .changeset/prepare-react-hydration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@codebelt/classy-store': minor
---

Add `prepareStoreForHydration(store)` to the React entry point. Call it on server and client stores initialized with matching data, before rendering or persistence, to retain the initial snapshot for server rendering and delayed Suspense hydration. Hooks then catch up to the live store without replacing server DOM, even if persistence changes state before their first read.

Preparation returns the original mutable proxy, preserves the first capture on repeated calls, and keeps current client subscriptions and core snapshots unchanged. Equal selector results retain their identity when hydration switches to live state. Add delayed and streamed hydration regressions, React 18/19 package coverage, and usage documentation.
7 changes: 7 additions & 0 deletions .changeset/react-compiler-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@codebelt/classy-store': patch
---

Fix stale auto-tracked React views when React Compiler or manual memoization reuses a derived value or child without repeating its property reads. Preserve committed dependencies for each reused immutable snapshot object, including structurally shared children, while isolating reads from suspended or abandoned renders.

Add compiled-consumer package tests for React 18 and 19, browser input and memoized-child regressions, and independent concurrency, hydration, and property-operation coverage. Selector APIs and synchronous React notification behavior are unchanged. Auto-tracking can conservatively retain earlier reads until a snapshot object's identity changes.
5 changes: 5 additions & 0 deletions .changeset/react-snapshot-target-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@codebelt/classy-store': patch
---

Reuse guarded backing copies of frozen snapshots across React auto-tracked consumers. Large forms and lists avoid repeatedly scanning and copying the same snapshot array while each hook retains its own tracked reads. Reflective mutations of tracking targets are rejected, and mutable computed values remain uncached.
13 changes: 13 additions & 0 deletions .changeset/release-retired-store-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@codebelt/classy-store': patch
---

**Runtime requirement:** auto-tracked React hooks now require native `WeakRef` support (ES2021). Selector hooks do not require `WeakRef`. See the [0.7.0 upgrade guide](https://codebelt.github.io/classy-store/docs/UPGRADING_0_7).

Release removed child proxies from their former parents, including children removed by native array-length truncation. Failed writes and deletes preserve the existing child connection; partial array truncations invalidate only the removed entries.

Discard obsolete untracked snapshot clones when a nested object starts using versioned tracking. This releases initial collection data and prevents fresh stores over the same raw object from receiving the stale cached clone.

Use weak render-read keys and a weak commit queue to prevent auto-tracked React consumers from retaining obsolete collection snapshots through shared child proxies. Isolate stable subscription and selector callbacks from render-local closure contexts so they do not retain initial results for the lifetime of a mount.

Add deterministic lifecycle and Compiler regressions plus production Chromium heap-snapshot checks for mounts, resets, rolling replacements, truncation, and intentionally retained detached children with Compiler off and on.
7 changes: 7 additions & 0 deletions .changeset/scoped-store-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@codebelt/classy-store': patch
---

Ignore pending persistence read results and read-error callbacks after unsubscribe. Retired handles no longer overwrite a current React scope or clear storage when an expired read completes after effect cleanup. Pending hydration promises still settle when storage completes.

Add a typed context example with independent and nested store instances, keyed resets, effect-owned persistence, and SSR hydration preparation. Test the actual example against packed React 18/19 consumers, including Strict Mode, storage configuration changes, asynchronous cleanup, and delayed Suspense hydration.
11 changes: 11 additions & 0 deletions .changeset/synchronous-react-subscriptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@codebelt/classy-store": minor
---

**Upgrade required for React consumers using timing options.** React subscriptions now always notify synchronously, fixing controlled-input caret jumps without configuration. Remove the `sync` option from `useClassyStore` and hooks returned by `createStoreHook`, and remove imports of `UseClassyStoreOptions`. Remove the entire options argument in auto-tracked mode, including an empty `{}`. Selector options still accept `isEqual`. Ordinary `subscribe()` callbacks remain microtask-batched and retain their existing `sync` option.

See the [0.7.0 upgrade guide](https://codebelt.github.io/classy-store/docs/UPGRADING_0_7) for before/after examples, the new native `WeakRef` requirement for auto-tracking, and SSR, Next.js, and persistence guidance.

React selectors may run after each mutation; several writes are not an atomic transaction. React can batch the resulting renders within an event handler. Auto-tracked reads are isolated per render so conditional and abandoned renders do not retain stale dependencies, including for class and null-prototype snapshots. Unchanged tracked references remain stable across parent renders.

Persistence hydration restores mutable nested defaults and custom merged values while preserving prototypes and user accessors. Add real-browser input regressions, lifecycle and SSR coverage, and packed-package checks for React 18 and 19.
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,30 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.3'
- run: bun install --frozen-lockfile
- run: bun run lint
- run: bun run test
- run: bun run typecheck
# Packed-consumer typechecks resolve the package's generated declarations.
- run: bun run build
- run: bun run typecheck
- run: bunx playwright install --with-deps chromium firefox webkit
working-directory: packages/classy-store
- run: bun run test:browser
- name: React scale commit budgets (production, Compiler off/on)
run: bun packages/classy-store/tests/performance/runPerformance.ts
env:
BENCHMARK_SAMPLES: '1'
BENCHMARK_ITERATIONS: '3'
BENCHMARK_LABEL: ci-smoke
- name: React memory retention (production, Compiler off/on)
run: bun packages/classy-store/tests/memory/runMemory.ts
env:
MEMORY_CYCLES: '6'
MEMORY_SIZE: '24'
MEMORY_LABEL: ci-smoke
5 changes: 5 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.3'
- run: bun install --frozen-lockfile
- run: bun run build
- run: bun run demos:build
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/react-compatibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: React compatibility

on:
pull_request:
branches: [main]

jobs:
react-compatibility:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
react: ['18.0.0', '18.3.1', '19.3.1']
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.3'
- run: bun install --frozen-lockfile
- run: bun run build
- run: node packages/classy-store/tests/compatibility/runCompatibility.mjs "${{ matrix.react }}"
- run: node packages/classy-store/tests/compiler/runCompiler.mjs "${{ matrix.react }}"
38 changes: 38 additions & 0 deletions .github/workflows/react-next.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: React Next.js integration

on:
pull_request:
branches: [main]
workflow_dispatch:

jobs:
next:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
compiler: ['off', 'on']
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.3'
- run: bun install --frozen-lockfile
- run: bunx playwright install --with-deps chromium
working-directory: packages/classy-store
- name: Packed production Next.js consumer (Compiler ${{ matrix.compiler }})
run: bun run test:react:next
env:
NEXT_TEST_MODES: ${{ matrix.compiler }}
- name: Save Next.js build and browser diagnostics
if: always()
uses: actions/upload-artifact@v7
with:
name: react-next-compiler-${{ matrix.compiler }}
path: test-results/next
if-no-files-found: ignore
retention-days: 7
8 changes: 5 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ jobs:
steps:
- uses: actions/checkout@v6
with: { fetch-depth: 0 }
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run build
- uses: actions/setup-node@v6
with:
node-version: '24'
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.3'
- run: bun install --frozen-lockfile
- run: bun run build
- name: Create Release PR or Detect Publishable Versions
id: changesets
uses: changesets/action@v1
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ website/build
website/.docusaurus
website/.cache-loader
website/static/demos

# Browser test artifacts
playwright-report
test-results
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@

Visit the **[Documentation Website](https://codebelt.github.io/classy-store/)** for tutorials, API reference, and examples.

## Upgrading to 0.7.0

React subscriptions now notify synchronously. Remove the React hooks' `sync` option and any `UseClassyStoreOptions` imports. Auto-tracked hooks now require native `WeakRef` support (ES2021). Selector equality options remain available, and ordinary core subscriptions still batch by default.

See the [0.7.0 upgrade guide and release notes](https://codebelt.github.io/classy-store/docs/UPGRADING_0_7) for before/after examples, SSR and Next.js setup, and persistence changes.

## 🚀 Features

- **Class-Based**: Define state and logic using standard ES6 classes.
Expand Down Expand Up @@ -55,10 +61,10 @@ function AddButton() {
}
```

For React controlled inputs, opt the field subscription into synchronous timing so React can preserve caret position and IME composition:
React subscriptions notify synchronously, so controlled inputs preserve caret position without extra options:

```tsx
const name = useClassyStore(formStore, (state) => state.name, {sync: true});
const name = useClassyStore(formStore, (state) => state.name);

return (
<input
Expand All @@ -68,6 +74,18 @@ return (
);
```

Ordinary `subscribe(store, listener)` callbacks still batch in a microtask. React batches renders within event handlers, while selectors can run after each mutation.

For server-rendered React apps, call `prepareStoreForHydration(store)` from `@codebelt/classy-store/react` on matching server/client stores before persistence starts. Delayed Suspense boundaries then hydrate from the original data and catch up to live state. See the [SSR guide](https://codebelt.github.io/classy-store/docs/REACT_SSR).

For Next.js App Router, construct stores inside Client Providers from serializable initial data. Use `@codebelt/classy-store/react/server` when preparing a store in server-only code. See the [App Router guide](https://codebelt.github.io/classy-store/docs/REACT_NEXT) for package boundaries, streaming, and navigation.

Selector and auto-tracked hooks are tested in React Compiler consumers on React 18 and 19. Auto-tracking preserves reads used by memoized calculations and children; no compiler opt-out is required. See [compiler compatibility](https://codebelt.github.io/classy-store/docs/#react-compiler-compatibility) for scope and render-filtering behavior.

Auto-tracked React subscriptions require native `WeakRef` support (ES2021) so tracking dependencies do not keep obsolete snapshots alive.

For independent forms, nested editors, and request-scoped state, see the [typed context example](https://codebelt.github.io/classy-store/docs/REACT_SCOPING). Providers own their store instances and effects; React keys control intentional resets.

## 📦 Installation

```bash
Expand Down Expand Up @@ -112,3 +130,9 @@ This library wouldn't exist without the ideas pioneered by these projects. Each
## 🤝 Contributing

See [CONTRIBUTING.md](./CONTRIBUTING.md) for details on how to contribute and the release workflow.

Run `bun run benchmark:react` to compare large forms and lists with selectors, auto-tracking, and React Compiler. See the [benchmark methodology](./packages/classy-store/tests/performance/README.md) for measurements and reproduction settings.

Run `bun run test:react:memory` to capture and compare Chromium heap snapshots across repeated mounts, resets, and collection changes. See the [memory regression methodology](./packages/classy-store/tests/memory/README.md) and [before/after results](./packages/classy-store/tests/memory/RESULTS.md).

Run `bun run test:react:next` for packed-package production Next.js builds and browser regressions with Compiler off/on. See the [integration methodology](./packages/classy-store/tests/next/README.md).
Loading
Loading