tracked() architecture: per-component signal scoping for React#32
Merged
scottmessinger merged 82 commits intomainfrom Mar 18, 2026
Merged
tracked() architecture: per-component signal scoping for React#32scottmessinger merged 82 commits intomainfrom
scottmessinger merged 82 commits intomainfrom
Conversation
Prototypes proving feasibility of compiled store reads: - Branded<T> type works with complex real-world interfaces - TypeScript compiler API detects $BRAND on PropertyAccessExpressions - Compiled reads 1.2-4.5x faster than solid-js/store - Direct signal reads with readSignal() helper - State library comparison benchmarks See issue #31 for full plan. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
createStore now returns [Branded<T>, SetStoreFunction] where Branded<T> recursively adds a phantom $BRAND marker at every object nesting level. This enables the Vite compiler plugin to identify store objects at compile time via the TypeScript type checker. - Export $BRAND symbol and Branded<T> type from @supergrain/core - Brand is optional ([$BRAND]?: true) so plain objects remain assignable - Array items are branded, arrays themselves are not - Fix pre-existing typecheck errors in benchmarks - Rename prototype type test to .typetest.ts (not a vitest suite) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
readSignal(target, prop) returns the signal for a property, creating it lazily on $NODE. The Vite plugin compiles store.title into readSignal(store, 'title')(). Accepts proxies or raw objects (calls unwrap internally). Shares signals with the proxy get trap. Also exports setProperty from @supergrain/core for direct writes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Vite plugin that compiles branded store property reads into direct signal access: store.title → readSignal(store, 'title')(). Uses TypeScript compiler API to detect $BRAND on resolved types. Handles nested access, skips writes/method calls, auto-inserts readSignal import. MagicString for source-mapped edits. 6 tests covering branded reads, nested access, writes, plain objects, method calls, and import merging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Simplified tracking hook for use with the Vite compiler plugin. Sets currentSub so compiled readSignal() calls track to the component's effect — no tracking proxy needed. useTrackedStore remains for backward compatibility (proxy-based tracking without the compiler plugin). 3 tests: identity return, reactive re-renders, fine-grained tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- hasBrand now checks for 'supergrain:brand' specifically instead of matching any __@ prefixed symbol property - useTracked saves/restores the previous subscriber via useLayoutEffect, preventing leaked subscribers across component boundaries - Document dev mode limitation in vite plugin (stale TS program) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ESLint rule that flags React components using readSignal() without calling useTracked(). Pattern-based detection on compiled output — if a function body has readSignal calls but no useTracked call, it reports an error. Same enforcement pattern as rules-of-hooks. 7 test cases: 4 valid, 3 invalid. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
readSignal inside .map() callbacks no longer flagged when the parent component has useTracked. exitFunction checks ancestor scopes and propagates hasReadSignal upward. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a “compiled reads” pipeline for @supergrain/core stores: createStore returns a branded type to enable a Vite TypeScript transform that rewrites store.foo reads into direct signal access via readSignal(...)(). It also adds a simplified React hook (useTracked) meant to work with compiled reads, plus an ESLint rule to enforce useTracked usage when readSignal is used.
Changes:
- Add
Branded<T>+$BRANDtocreateStoretypes and introducereadSignal(target, prop)in core. - Add
@supergrain/vite-pluginthat rewrites branded property reads intoreadSignal(...)()and injects imports. - Add
useTrackedin@supergrain/reactand an@supergrain/eslint-pluginrule (require-use-tracked) with tests.
Reviewed changes
Copilot reviewed 53 out of 55 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Adds a dedicated node-environment project for eslint-plugin tests. |
| tsconfig.json | Adds TS project references for new workspace packages (vite-plugin, eslint-plugin). |
| packages/vite-plugin/vite.config.ts | Library build config for the new Vite plugin package. |
| packages/vite-plugin/tsconfig.json | TS config for vite-plugin (composite/declarations/outDir). |
| packages/vite-plugin/tests/transform.test.ts | Unit tests for branded-read transform behavior. |
| packages/vite-plugin/src/plugin.ts | TS compiler API transform + import injection + Vite plugin wrapper. |
| packages/vite-plugin/src/index.ts | Exports supergrain() plugin entrypoint. |
| packages/vite-plugin/package.json | New package metadata/deps/scripts for @supergrain/vite-plugin. |
| packages/react/tests/use-tracked.test.tsx | React tests validating useTracked + readSignal re-render behavior. |
| packages/react/src/use-store.ts | Adds useTracked hook intended for compiled reads. |
| packages/react/src/index.ts | Exports useTracked from @supergrain/react. |
| packages/js-krauset/src/main.tsx | Updates imports to include $BRAND/Branded (currently unused). |
| packages/eslint-plugin/vitest.config.ts | Vitest config for eslint-plugin tests. |
| packages/eslint-plugin/tsconfig.json | TS config for eslint-plugin (composite/outDir). |
| packages/eslint-plugin/tests/require-use-tracked.test.ts | Tests for require-use-tracked rule. |
| packages/eslint-plugin/src/rules/require-use-tracked.ts | ESLint rule implementation enforcing useTracked with readSignal. |
| packages/eslint-plugin/src/index.ts | ESLint plugin entry exporting rules. |
| packages/eslint-plugin/package.json | New eslint-plugin package metadata (currently points main to TS sources). |
| packages/core/tests/read-signal.test.ts | New tests validating readSignal reactivity + proxy interop. |
| packages/core/tests/branded-type.test.ts | Type-level tests for Branded<T> + runtime sanity checks. |
| packages/core/src/store.ts | Adds $BRAND, Branded<T>, readSignal, and updates createStore return type. |
| packages/core/src/index.ts | Exports readSignal, setProperty, $BRAND, and Branded. |
| packages/core/prototype/vite-plugin.ts | Prototype plugin (regex-based) for earlier experimentation. |
| packages/core/prototype/vite-plugin-bench/vite.config.ts | Prototype bench Vite config. |
| packages/core/prototype/vite-plugin-bench/src/bench.ts | Prototype bench harness for rewritten vs proxy reads. |
| packages/core/prototype/vite-plugin-bench/check-transform.ts | Script to print prototype transform output. |
| packages/core/prototype/realistic.bench.ts | Prototype “realistic” benchmark scenarios. |
| packages/core/prototype/preact-store.ts | Prototype preact-signals store implementation for benchmarking. |
| packages/core/prototype/preact-store-correctness.test.ts | Correctness tests for preact-store prototype. |
| packages/core/prototype/plugin-feasibility/input.ts | Feasibility input for TS compiler detection of branding. |
| packages/core/prototype/plugin-feasibility/detect-branded.ts | Feasibility script for brand detection via TS checker. |
| packages/core/prototype/model.ts | Prototype model implementation and benchmarks scaffold. |
| packages/core/prototype/model.test.ts | Prototype model correctness tests. |
| packages/core/prototype/model.bench.ts | Prototype model benchmarks. |
| packages/core/prototype/direct-signal-reads.bench.ts | Benchmarks for “direct signal read” approach. |
| packages/core/prototype/direct-signal-correctness.test.ts | Correctness tests for “direct signal read” approach. |
| packages/core/prototype/computed-vs-signal.bench.ts | Benchmarks comparing computed vs signal call overheads. |
| packages/core/prototype/compiled-vs-stores.bench.ts | Benchmarks comparing compiled approach vs other stores. |
| packages/core/prototype/compiled-reads.bench.ts | Benchmarks focused on compiled reads. |
| packages/core/prototype/compiled-reads-and-writes.bench.ts | Benchmarks for compiled reads plus compiled writes. |
| packages/core/prototype/compiled-correctness.test.ts | Correctness tests for benchmark scenarios. |
| packages/core/prototype/compiled-alien-vs-preact.bench.ts | Benchmarks comparing alien vs preact primitives under same architecture. |
| packages/core/prototype/check-versions.mjs | Script to print installed versions for benchmark context. |
| packages/core/prototype/call-overhead.bench.ts | Benchmarks for per-call overhead in effects. |
| packages/core/prototype/branded-type.typetest.ts | Large type-level validation suite for Branded<T>. |
| packages/core/package.json | Updates core package metadata/deps/scripts (includes version change). |
| packages/core/benchmarks/state-libraries.bench.ts | Adds cross-library benchmarks (zustand/jotai/mobx/valtio/preact). |
| packages/core/benchmarks/STATE_LIBRARIES.md | Documents benchmark results/interpretation. |
| packages/core/benchmarks/ROW_OPERATIONS.md | Documents row operation benchmark results. |
| packages/core/benchmarks/CORE_COMPARISON.md | Documents core vs solid-js/store benchmark results. |
| packages/core/benchmarks/ADDITIONAL.md | Documents additional benchmark results and analysis. |
| packages/core/PLAN-model-api.md | Design/plan doc for the compiled reads approach and plugin. |
| notes/failed-approaches/inline-primitive-checks-optimization.md | Postmortem doc for a reverted micro-optimization attempt. |
| .claude/settings.local.json | Updates local Claude tool allowlist (dev-only config). |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Unified to one hook name: useTracked. The proxy-based implementation works with or without the Vite compiler plugin. No separate hooks for compiled vs proxy paths. Also adds packages/js-krauset-compiled/ for A/B benchmarking the compiled reads path against the proxy path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add vitest workspace config to packages/core with two projects: proxy (no plugin) and compiled (with supergrain vite plugin). Every core test now runs in both modes automatically. - Fix vite plugin import injection: add readSignal to the existing import source instead of hardcoding @supergrain/core (fixes dual-module bug). - Change readSignal to return wrap(node()) so bracket access and dynamic property reads work correctly through compiled boundaries. - Add typed overload to readSignal so the plugin can resolve Branded<T> through chained readSignal calls. - Change plugin to build nested readSignal expressions for full property chains (store.a.b.c → readSignal(readSignal(readSignal(store,'a'),'b'),'c')). - Add compiled-comparison.bench.ts benchmarking proxy vs compiled reads. - Delete packages/core/prototype/ (replaced by real plugin + benchmarks). - Document per-level readSignal approach in notes/failed-approaches/. - Upgrade vitest 3.2.4 → 4.1.0 across all packages. - Fix vitest 4 breaking changes: browser.provider factory, context imports, matchers setup, node_modules exclusion, tsconfig types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core changes: - Add readLeaf() — skips wrap() for primitive reads (no proxy creation) - Add initSignals() — eagerly creates signals for all properties at createStore time and when new objects are set via setProperty. Required for inlined $NODE access to work without fallback branches. - Make $NODE configurable to avoid proxy invariant violations - Add typed overload to readSignal preserving Branded<T> through chains - Export readLeaf from @supergrain/core Benchmarking: - Add real-overhead.bench.ts isolating per-operation costs: cached $NODE is 14x faster than proxy for 3 leaf reads - Update compiled-comparison.bench.ts (reactive scenarios only) - Hand-write compiled krauset benchmark (js-krauset-compiled) showing what the compiler should produce: useCompiled + direct $NODE reads - Add compiled-vs-proxy.test.tsx in react package: correctness tests for all krauset operations (create, update, select, swap, remove, clear) in both proxy and compiled modes Key finding: direct signal call via cached $NODE is 10-14x faster than proxy, but the plugin can't yet generate this form reliably. The hand-written compiled krauset benchmark demonstrates the target output. Known limitation: compiled mode doesn't detect array mutations ($pull, splice) because the data signal reference doesn't change — needs $OWN_KEYS tracking which the compiled path doesn't have yet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Captures key findings from earlier conversations: - Alien-signals vs preact-signals benchmarks (preact 2-4x faster reads) - Why solid-js is faster (compiler eliminates proxy reads, 60x proxy overhead) - Computed caching architecture (zero overhead, beat solid in every benchmark) - Transcript locations for future agent exploration - Open questions that were never resolved Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…analysis Proxy optimization: - Add Solid-style fast path to proxy get trap: check for already-tracked signal BEFORE symbol checks. On repeat reads (the common case), this is 3 operations instead of ~13. Improves proxy reads by ~12%. Class getter discovery: - V8 inlines class prototype getters to near-bare-signal speed (4,506 ops/s) but CANNOT inline: function calls (541), Object.defineProperty getters (1,142), or proxy traps (472). This is the same reason preact signals (.value getter) are 2-4x faster than alien-signals (function call). - Class getter reads are 9.5x faster than proxy for reactive leaf reads and 6.3x faster for component render (6 props). - Hand-written StoreView class demonstrates the target compiler output. Research documentation: - notes/research/path-to-10x.md: Full synthesis of findings with action plan - notes/research/prior-conversation-findings.md: Transcript locations and prior research summary - benchmarks/exhaustive-read-patterns.bench.ts: Every possible read pattern ranked by ops/s Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All three modes (proxy, compiled, class-getter) pass all 6 krauset operations: create 1000 rows, update every 10th, select, swap, remove, clear. 66 tests total. Class getter approach uses a hand-written AppStateView class with prototype getters that call signals directly. V8 inlines these to ~4,500 ops/s (10x faster than proxy at ~470 ops/s in micro-benchmarks). Refactored test to share Row component and effect setup across modes. useClassView hook caches view instances per raw object. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Real browser benchmark results (chromium via playwright): - Create 1000 rows: class-getter 1.14x faster - Update every 10th: class-getter 1.40x faster - Select row: class-getter 1.07x faster - Swap rows: class-getter 1.56x faster The 10x micro-benchmark advantage translates to 1.1-1.6x end-to-end because React's reconciliation dominates total render time. Store reads are a small fraction of the work. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
End-to-end browser benchmark (chromium, all operations): - Create 1000 rows: class-getter 1.17x faster (57ms vs 67ms) - Replace 1000 rows: class-getter 1.09x faster - Partial update (100/1000): class-getter 1.21x faster - Select row: class-getter 1.26x faster (54ms vs 68ms) - Swap rows: class-getter 1.57x faster (60ms vs 94ms) - Clear rows: class-getter 1.09x faster 10x micro-benchmark advantage translates to 10-25% end-to-end because React reconciliation dominates. But consistent improvement across all operations — worth pursuing if combined with other optimizations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three approaches benchmarked end-to-end in browser: - proxy: useTracked + For (current) - class-getter: AppStateView with V8-inlined getters - fine-grained: each row subscribes to 'selected' independently Results: - Create: fine-grained 1.24x faster (fewer parent re-renders) - Select: class-getter 1.10x faster (fine-grained causes 1000 effect checks) - Swap: class-getter 1.94x faster - Partial update: class-getter 1.23x faster Fine-grained has a tradeoff: faster initial render but slower on selection changes because every row's effect fires. Class-getter is the most consistent winner. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createView(store) returns a lightweight view object with prototype getters that read signals directly, bypassing the proxy. View objects are cached per raw object (WeakMap) and prototypes are cached per property-key set (Map). Benchmark confirmed: dynamic prototype getters match static class getters in V8 (~4,100 ops/s, 8x faster than proxy). The critical requirement is getters on a PROTOTYPE, not on instances. End-to-end krauset browser benchmark results (chromium): - Create 1000 rows: createView 1.37x faster than proxy - Swap rows: createView 1.46x faster - Partial update: createView 1.48x faster - Select row: tied (within variance) All 142 core tests pass (proxy + compiled modes). All 66 react tests pass. All packages green. Getter pattern benchmark at packages/core/benchmarks/getter-patterns.bench.ts confirms all dynamic prototype approaches (Function+prototype, setPrototypeOf, new Function class) perform identically to static class getters. Only per-instance defineProperty is slower (4x). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createModelStore(schema, data) returns [proxy, update, view] where the view prototype is built from ArkType schema props at definition time. Nested schemas generate nested view prototypes recursively. Core micro-benchmarks: - Model store leaf reads: 4,836 ops/s (10.2x faster than proxy) - Model store 6-field render: 1,184 ops/s (2.4x faster than proxy) - Reactive updates/batching: equivalent to proxy 9 new tests for model-store: schema creation, reactive reads, writes, nested views, sub-tree replacement, fine-grained isolation, prototype sharing. All pass. Cleaned up krauset browser benchmark to remove model-store entries (arktype not available in browser context). createView entries remain as the browser-testable prototype-getter approach. 160 core tests, 66 react tests, all packages green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DirectRow subscribes signals to DOM refs via useEffect, bypassing React's reconciliation for subsequent updates. Initial render still goes through React. Results: direct-dom shows modest improvement on select (1.15x) and swap (1.74x), but is slower on partial update — the per-row effect subscription setup cost (2 effects × 1000 rows) dominates in the current benchmark structure where create+operate are measured together. The direct-dom approach's real advantage is on subsequent updates after initial render, which this benchmark doesn't isolate well. The setup cost of 2000 effect subscriptions per iteration masks the DOM update savings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Solid-style approach: React renders outer container, rows created via cloneNode(true) + direct signal subscriptions to DOM nodes. No React components, no VDOM, no memo for rows. Results (chromium, end-to-end): - Create 1000 rows: 2.3x faster (31ms vs 71ms) - Select row: 2.3x faster (32ms vs 73ms) - Swap rows: 3.6x faster (27ms vs 95ms) - Partial update: 1.8x faster (39ms vs 70ms) This is the performance ceiling for a React + signals architecture. It matches solid-js's approach: template cloning + direct DOM bindings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
$$() is an identity function exported from @supergrain/core. It marks
reactive expressions for direct DOM binding. Without the compiler, it
returns the value unchanged (graceful degradation). The compiler will
transform $$() calls in JSX to ref + effect subscriptions.
useDirectBindings hook (from @supergrain/react) takes an array of
{ ref, getter, attr? } bindings and wires alien-signals effects
directly to DOM nodes, bypassing React re-renders:
- Text position: updates ref.current.textContent
- Attribute position: updates ref.current[attr]
6 new tests: text updates, className updates, multiple bindings,
zero React re-renders, cleanup on unmount, computed expressions.
160 core tests, 72 react tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The plugin now detects $$() calls in JSX and transforms them into
ref + useDirectBindings for direct DOM updates bypassing React.
Transformation:
{$$(item.label)} → ref on parent element + useDirectBindings text binding
className={$$(...)} → ref on element + useDirectBindings attr binding
For each component containing $$() calls, the plugin:
1. Generates useRef declarations (__$$0, __$$1, ...)
2. Generates useDirectBindings([...]) with all bindings
3. Adds ref={__$$N} to target JSX elements
4. Strips $$() wrapper, leaving just the inner expression
5. Injects useRef (react) and useDirectBindings (@supergrain/react) imports
Arrow function expressions in $$(() => expr) are preserved as getters.
Plain expressions in $$(expr) are wrapped in () => expr.
10 plugin tests pass (6 existing + 4 new).
160 core tests, 72 react tests unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DirectFor now uses alien-signals effect to watch array structure (length + element references). Catches in-place mutations (splice, push, swap) that don't change the array reference. Key fixes: - useLayoutEffect depends on [each] for reference changes (store.data = x) - Inner effect subscribes to every array index for structural changes - Row effects created outside reactive context (no nested overhead) - Cleanup tears down both outer + row effects Added 5 new tests matching krauset operations: - create 1000 rows, swap rows, remove row, append rows, clear All 95 react tests pass. Zero type errors. Also fixed js-krauset-direct to use containerRef + forceUpdate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert .claude/settings.local.json to main to keep local-only permissions out of the PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move all planning docs to appropriate folders (architecture, performance, failed-approaches, research) based on status - Delete path-to-10x.md (fully superseded by compiled-reads-investigation.md) - Remove .vitest-attachments from tracked files - Add .claude/settings.local.json to .gitignore - Clean up duplicate .vitest-attachments gitignore entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
V2 contains the root cause analysis, before/after numbers, and complete solution. V1 adds no unique context and would confuse future agents. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add TL;DR/status block at top of every doc - Standardize structure: goal, approach, outcome, learnings - Failed approaches: prefix titles with FAILED, surface failure reason immediately - Remove redundant prose (~7500 lines net reduction) - Convert verbose paragraphs to tables where data is involved - Fix cross-references for moved files (planning/ → current paths) - Update README.md to reflect current folder structure - Comparisons: cut 50-70% verbose source citations, keep data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- v2-initial-design: cut from 670 to ~95 lines, mark as historical - v4/nested-components-solution: trim redundancy, point to v5 - v5-final: add missing cleanup useEffect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Architecture: add status blocks, trim ember-analysis and react-adapter-architecture (~60% reduction), mark superseded content. Benchmarks: add TL;DR to all 14 files, convert prose to tables, note superseded/duplicated docs, cut verbose recommendations. Failed approaches: add FAILED: prefix to all 6 titles, surface failure reasons front-and-center, collapse verbose step-by-step narratives into tables (~70-85% reduction per file). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…undant docs
Structure changes:
- Eliminate research/ folder — files distributed to where they belong
- compiled-reads-investigation → performance/ (capstone perf doc)
- reactively-takeaways → merged into comparisons/reactively.md
- ember-analysis → comparisons/ember.md (external system analysis)
- signal-pooling-benchmark-code → benchmarks/signal-pooling.md
File renames for clarity:
- core-benchmarks-readme → running-benchmarks
- consolidated-findings → findings-summary
- js-benchmark-plan → krausest-setup
- performance-plan-v2 → core-store-optimization
- v5-final → useTracked (name the thing, not the version)
- solid-architecture → solid, storable → supergrain
- techniques → reactive-techniques
Merges:
- structurae-evaluation + structurae-final-assessment → single file
- reactively + reactively-takeaways → single file
Deletions (10 redundant files):
- benchmarks: analysis, benchmarks, proxy-overhead-{summary,benchmark},
results, performance-analysis (all superseded)
- research: main-plan (unstarted), prior-conversation-findings (redundant)
- react-adapter: nested-components-solution (redundant with useTracked)
All cross-references updated across remaining files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…es doc Restored (had unique content, not redundant): - benchmarks/performance-analysis.md — benchmark bug correction methodology - benchmarks/results.md — proxy vs direct signal data (referenced by findings-summary) - benchmarks/proxy-overhead-benchmark.md — benchmark source code Added: - failed-approaches/react-tracking-approaches.md — documents all 7 failed React tracking approaches with failure reasons (extracted from v2-v4 history) Updated: - README.md — comprehensive index with descriptions, categorized failed approaches - benchmarks/README.md — restored references to all files in the folder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The file was renamed but the internal reference still pointed to the old name. Updated to point to README.md and fixed the H1 title. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core fixes: - Fix deleteProperty on arrays: splice/pop/shift were throwing "Direct deletion not allowed". Now allows silent deletes on arrays with ownKeys bump for structural subscribers. - Make $VERSION a signal: bumpVersion now writes to a signal in the node map instead of incrementing a plain number. This enables components to subscribe to "any mutation on this array" without subscribing to every individual index signal. Alien-signals deduplicates dirty-marking so 1000 writes during splice cost ~0.2ms. - Auto-subscribe to array version: when the reactive proxy returns an array value with an active subscriber, automatically subscribe to the array's version signal. This ensures parent components detect in-place mutations (splice, push, swap) without explicit $TRACK reads. - trackSelf on array function access: calling .map/.forEach/etc on a reactive proxy array subscribes to ownKeys when getCurrentSub is set. These changes enable the tracked() architecture where each component independently subscribes to exactly the signals it reads, achieving 7.5x faster partial updates and 4.7x faster swaps vs the current useTracked approach. Benchmark packages: - js-krauset: add krauset compliance tests (9 ops) + perf tests - js-krauset-direct: add exports, vitest config, compliance + perf tests - react: add splice, swap, append, clear, partial update tests Vite plugin: - Fix CJS output extension (.cjs) for type: module packages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Detailed walkthrough of 5 experimental approaches: - DirectFor (full DOM rebuild) — failed, catastrophic on structural mutations - createView/useView (getter-based reads) — failed, marginal improvement - $$() direct bindings — failed, double work with React - useScopedTracked (per-component proxy) — partial, overhead from proxy wrapping - tracked() component wrapper — success, 7.5x partial update, 4.7x swap Includes source code, benchmark numbers, bug analysis, and key lessons. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds: splice bug discovery, version signal vs ownKeys tradeoff, stale dependency analysis, why For is still needed, why console.log fixed the bug, per-field component alternative, explicit store.selected read pattern, safe-on-non-reactive-components analysis, and ships-vs-doesn't-ship summary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds: alien-signals background, side-by-side label change trace (old vs new architecture), "wrap the render not the reads" key insight bridging Experiments 4→5, .map() vs For experiment (5b), why For's version-based memo is redundant with tracked(), concrete stale dependency example, clearer store.selected boundary explanation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes alien-signals background (covered in react-tracking-approaches.md), redundant createStableProxy code (in useTracked.md), verbose experiment code. Adds references to existing docs, connects tracked() to previously rejected "global subscriber" approach (#1 in react-tracking-approaches.md), adds "For as render boundary" insight. ~30% shorter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tracked() wraps the component definition instead of being called inside it. Sets currentSub once per render (O(1)) instead of per property access (O(n)). Each component independently subscribes to exactly the signals it reads — parent reads don't leak into children. Result: label change on row 5 re-renders only Row 5 (not App + 1000 memo checks). 7.5x faster partial updates, 4.7x faster swaps. Removed: - useTracked hook - createStableProxy, globalProxyCache, proxyEffectMap - All useTracked imports and usage across 25 files Added: - tracked() in packages/react/src/tracked.ts - 11 tracked() tests in packages/react/tests/tracked.test.tsx Converted: all tests, benchmarks, examples, and doc-tests to tracked(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- README: replace all useTracked examples with tracked() pattern - For component: remove version prop cloning (redundant with tracked()), simplify to key-only cloneElement for keyed reconciliation - tracked.ts: remove unnecessary `as unknown as FC<P>` cast, let TypeScript infer MemoExoticComponent return type from memo() - useTracked.md: mark as superseded by tracked() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- js-krauset main.tsx: remove (tracked as any) casts (types resolve after react package rebuild), remove stale version prop JSDoc - write.ts: remove stale useView reference in deleteProperty comment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- for-component-magic.test.tsx: remove versionSymbol lookup (For no longer passes version props) - render-analysis.test.tsx: remove $VERSION usage, version props, and two tests that were entirely about the old version prop mechanism Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updated 13 docs: README index, architecture docs (react-adapter, vite-compiler-plugin, app-store-plan), comparison docs (reactively, solid, zustand, valtio, supergrain, rxjs, mobx, jotai). Left failed-approaches/ and historical react-adapter/ versions unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tooling: - Add oxfmt for formatting (216 files formatted) - Remove eslint config (.eslintrc.json) and prettier (.prettierrc, .prettierignore) — replaced by oxlint + oxfmt - Add root scripts: lint, format, format:check - Update lint scripts to -D correctness -D suspicious on src/ only - Fix React types in benchmark packages (19.0.0 → ^19.1.13) Lint fixes: - Remove unused imports (memo, FC, useReducer, useCallback, etc.) - Remove unused variables and parameters (prefix with _) - Remove dead versionSymbol code from for-component-magic test - Remove dead version prop tests from render-analysis test - Fix direct-for.tsx unused useReducer import Notes docs: - Update 13 docs to reference tracked() instead of useTracked Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ugin Deleted packages: - packages/vite-plugin/ — only purpose was $$() compiler transform - packages/js-krauset-direct/ — used DirectFor (failed experiment) - packages/react-example/ — unused example package Deleted files: - react/src/direct-for.tsx — full DOM rebuild, catastrophic on mutations - react/src/use-direct-bindings.ts — $$() and useDirectBindings - react/tests/direct-for.test.tsx, direct-binding.test.tsx, pipeline.test.tsx - core/tests/read/create-view.test.ts Removed from source: - createView, getSignalGetter, defineSignalGetter, viewCache, signalGetterCache from core/src/read.ts - createView exports from core/src/store.ts and core/src/index.ts - DirectFor, $$, useDirectBindings exports from react/src/index.ts Updated: - README.md: removed $$(), createView, DirectFor sections - CI: added lint and format:check steps - Lint scripts: enabled pedantic + style categories on src/ - .changeset/config.json: removed react-example from ignore list - tsconfig.json: removed vite-plugin reference - vitest.config.ts: removed react-example test inclusion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add .oxlintrc.json disabling inappropriate rules (no-magic-numbers, id-length, no-ternary, sort-keys, sort-imports, func-style, no-null, func-names, init-declarations, max-lines, max-lines-per-function, new-cap). Code fixes across all packages: - Add curly braces to all single-line if/for/while bodies - Replace Object.prototype.hasOwnProperty.call with Object.hasOwn - Replace for-in with for-of Object.keys() - Use T[] instead of Array<T> - Use TypeError for type-checking errors - Reduce function params (options objects) - Reduce nesting depth (extract helpers) - Use arrow body style, destructuring, template literals - Merge duplicate imports - Use consistent type definitions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete packages/app-store/ (orphaned directory, no source code) - Remove use-sync-external-store from root and react package (vestigial from old useTracked, not imported anywhere) - Remove @types/use-sync-external-store from react devDependencies - Fix doc-tests vitest.node.config.ts: app-store/src → store/src Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- tracked.ts: null out ref.current on cleanup so StrictMode remount creates a fresh effect (fixes dead effectNode bug) - core.ts: remove unused .$ property from Signal interface and getNode (set but never read anywhere) - read.ts: fix misleading "frozen objects" comment — the early return handles frozen objects, the catch handles sealed/ non-configurable objects Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- tracked.ts: null ref.current on cleanup so StrictMode remount creates a fresh effect (#1 from review) - store/types.ts + document-promise.ts: expose error getter on DocumentPromise interface and implementation (#9) - store/store.ts: memoize findDoc by (modelType, id) to avoid creating duplicate computeds. Replace spread copy of document map with direct path set (#5) - core/core.ts: remove unused .$ property from Signal (#12) - core/read.ts: fix misleading frozen objects comment (#11) - core/operators.ts: clarify pullFromArray comment — operates on raw array, manual signal management is necessary (#7) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enable 7 previously-disabled lint rules (require-await, init-declarations, func-names, new-cap, unicorn/text-encoding-identifier-case, unicorn/prefer-global-this, unicorn/no-useless-undefined) - Fix violations: name anonymous functions in typed.ts, inline disables for legitimate exceptions (React component calls, signal setters) - Remove lint from doc-tests package (only core/react/store are linted) - Enable import sorting in oxfmt with grouped ordering - Fix @ts-nocheck placement in benchmark files after import reordering - Remove .claude/settings.local.json from git tracking Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
useTrackedwithtracked()— a component wrapper that gives each component its own signal subscription scopeThe tracked() architecture
tracked()wraps a component definition. It setscurrentSubonce before the component function runs and restores it after. All reactive proxy reads during the component's render are tracked to that component's own effect.Key property:
Component(props)is synchronous — children render later in separate React calls. SocurrentSubdoesn't leak into children. Eachtracked()component independently subscribes to exactly the signals it reads.Result: label change on row 5 re-renders only Row 5. App doesn't re-render. No 1000-item memo check cascade.
Benchmark results (apples-to-apples, same Row template)
Faster or equal on every operation. Zero regressions.
Core changes
bumpVersionwrites to a signal. Alien-signals deduplicates dirty-marking so 1000 writes during splice cost ~0.2ms (vs 999 ownKeys bumps at ~30ms)..map(),.forEach()etc. subscribe to ownKeys when there's a subscriber.Removed (failed experiments)
Tooling
Test plan
🤖 Generated with Claude Code