Changelog
All notable changes to this project are documented here.
Unreleased
2.1.0 - 2026-10-07
Read-only types for what readers get and for params, and a fix to the default isEqual of select, the one change at runtime. The minor version is for the types: code that writes to what useStore returns, to storeRef(params).get() or inside select no longer compiles.
Changed
- Types: what readers get is read-only. The keys of
StoreState, whichuseStorereturns,storeRef(params).get()returns, andselectandsubscribelisteners receive, arereadonly, sostore.count = 1,delete store.countor a write insideselectis a compile error. At runtime only a write to the proxy is refused, and only in development;selectreceives the store's own data, where a write changed it for every reader without notifying them. Params areReadonly<Params>inuseStore,storeRef, the store hook and the testing helpers: their values name the instance, and a reader reads them again only when it gets a different object, so pass a new object rather than changing one you passed. Only the keys are read-only: arrays and objects under them keep their types, and a read-only key still passes where a mutable type is expected. AI_CONTEXT.md, the guide for AI assistants, follows the docs' principles: the model (a store as a small service, lazy, eventually consistent), the ten rules of the Rules page in the same order, and a golden path in the four store layers (domain, a core store whose command returns an outcome and whose failed request is state, a UI store and a plain hook, views that check before reading) instead of a counter. It no longer says to mountAutoRootCtxat the top and never to use providers: it goes inside the providers the stores read. CI type-checks its code blocks with the docs'.
Fixed
- The default
isEqualofselect(inuseStoreanduseMultipleStore) skipped the holes of a sparse array: a selection that went fromnew Array(3)to[, 42, ]counted as unchanged, so the component kept showing the old value, andshallowEqual(new Array(1), [42])was true whileshallowEqual([42], new Array(1))was false. It now compares every index, and a hole differs fromundefined, sincemapskips a hole. Arrays without holes compare as before.
2.0.4 - 2026-10-07
Documentation only. The built code is the same as 2.0.3's; the package's README, which npm shows, is the shorter one below.
Fixed
- Docs: two examples did not type-check. Error handling reported through a function named like the browser's
reportError, which takes one argument; the example of a proxy passed to a child passed props to aRowthat took none. CI now type-checks the TypeScript blocks of the docs and the README against the library (yarn test:docs-code), so an example using a removed export, an unknown option or a wrong argument fails the build. - Docs: Introduction and Limitations quoted older benchmark ratios (1.6–2.6x Jotai's time in jsdom, 1.1–1.6x in Chrome) instead of the current 1.3–2.1x and 1.1–1.3x, and Testing said the readers of a mock that throws keep its last values, as in 1.x; they throw its error.
Changed
- Docs: two entry pages. Rules lists in one page what the library adds to the rules of hooks, each with a link to its page; For React developers maps what a React developer knows onto stores, compares the library with Jotai, RTK Query, Zustand and Redux (moved from the README), and says what it costs. The sidebar starts with them, and keeps the patterns (progressive data, realtime, events) and the integrations (testing, dev tools, SSR, concurrent rendering, React Compiler) in collapsed groups.
- Docs: a shorter way in. The README drops its How it works and Quick Start sections and the long comparison (now in For React developers) for a summary of the API and a paragraph on Jotai, RTK Query and Zustand: 1,986 words to 1,188. The Introduction page is folded into the home page, which gains its When it fits section; its old address sends readers there, and the Guide link opens Getting started. Limitations says what an update costs in one paragraph and leaves the rest to How it works.
- Docs: Organizing stores in layers (domain, core stores, UI stores and hooks, views) is on the way in: in the Start group of the sidebar, linked from Getting started, Rules, the home page and the README, and summed up in For React developers.
- Docs: the home page names what 2.0 adds (
useMultipleStore, schedules, a store that throws failing alone) and links Migrating to 2.0.
2.0.3 - 2026-10-06
Changed
- Faster readers. A
useStorecall holds its instance with one state, one ref and one effect, and the proxy form reads failures through its own subscription: 5 hooks and 2 effects per reader instead of 12 and 5. Updates that re-render readers take 20–38% less time in the jsdom benchmarks (React's development build) and up to 18% less in headless Chrome (production build), 41% where 4920 readers re-render. The selector form ({ select }) reads failures through its own subscription too and keeps its subscription on the reader: 6 hooks instead of 9, 10–16% faster with 1000 readers (production build, jsdom).
Fixed
- In development with React 19, a component that passed its
useStoreproxy (or the proxies ofuseMultipleStore) to a child as a prop re-rendered on every change of the store, and could warn that it spread the state. React's development build reads every key of such a prop to diff it, in the commit, and those reads counted as the component's. They no longer do, and they do not warn. Production builds were not affected.
2.0.2 - 2026-10-06
Fixed
useSyncExternalStore(ref.subscribe, ref.get)rendered until React gave up ("Maximum update depth exceeded"):get()built a new object on every call.storeRef(params).get()now returns the same snapshot until the state changes, frozen since every caller shares it.- A store hook returning a tuple (
createStore('count', () => useState(0))) type-checked, then crashed its readers with "useStore is not a function or its return value is not iterable"; one returning nothing published nothing, silently. A hook returning an array, a function or no object is now a type error, and in development its instance fails with aTypeErrorthat says to return an object.
Changed
- Reading a store action through the
useStoreproxy in an event handler (store.increment?.()) no longer logs the "read outside of render" warning: an action needs no subscription. The warning for values now names the case it missed: a child component reading a proxy passed to it as a prop, in a render of its own. - The console message saying which store a throwing hook disabled is logged in development only. React logs the error itself in every build, and hands it to
onCaughtError.
2.0.1 - 2026-10-06
Fixed
- A failed store with a
timeToCleankept its failed instance until the timeout, and forever withInfinity, so a reader coming back after an error boundary's retry threw again. A failed instance is now torn down as soon as nothing reads or retains it, and at once if it fails while waiting out itstimeToClean. Aretain()still keeps it, failed, until released. - A new instance kept the keys an earlier one left in the context: a store started afresh while a reader held its context (remounting
AutoRootCtxwith a newkey, or after a failure) still showed the old instance's values, and a hook restarted by a hot update kept the keys only the old hook returned. The first publish of an instance now drops them. - A store moving between
AutoRootCtxin two React roots: the instance it left cleared the failure of the one it moved to when it unmounted, and an instance that had failed in the old root stayed failed once running in the new one. Fast Refresh remounting a failed store had the same issue. - A hook that threw
undefinedornullleftstoreRef(params).erroratundefined, as if the store were running, and its readers threwundefined. It now fails with anErrorthat says so, with the thrown value as itscause.
Changed
- The
UseStoretype, the type of a store'suseStorethatStorealready uses, is exported. - The dev tool marks a store whose hook has not returned yet "not published" instead of "not running": a store that suspends is running.
2.0.0 - 2026-10-06
The API is createStore, which returns useStore and storeRef, plus useMultipleStore and <AutoRootCtx />: stores that are lazy, shared, automatic and composable. 1.10 has this API next to the old one, with everything below marked @deprecated: move to it on 1.10, with the app working at every step, then upgrade. See Migrating to 2.0 in the docs.
Removed
initialState. Every key isundefineduntil the store has run once, and typed as optional: default at the read (const { count = 0 } = useStore()). On the serveruseStorereturns{}.AttachedComponent, a number ascreateStore's options and its fourth argument. Write the side effect in the store hook; pass{ timeToClean }.- The
preStateargument of the store hook. A hot update that changes the hooks a store calls restarts it from its own initial state, as Fast Refresh does with a component. useStore(params, selector, isEqual)anduseStore(selector): useuseStore(params, { select, isEqual }).getStore(usestoreRef),useStoreSuspense,useStoreStatusanduseCtxState.StateScopeProviderand scopes: put what tells instances apart in the params; remountAutoRootCtxwith a newkeyto start every store afresh.- The
Wrapperanddebuggingprops ofAutoRootCtx, andStoreErrorBoundary. Each store keeps its own error boundary; report errors with React'sonCaughtError, inspect stores with the dev tool. - From the main entry:
createRootCtx,createAutoCtx, theContextclass and its hooks,useArrayChangeId,paramsToId,shallowEqual,formatState, the schedulers,scheduledanduseFrameState(import those fromreact-state-custom/schedulers), and the typesStoreHandle,StoreStatus,StoreStateWith,StateDebugRenderer,StoreSelectOptions,ParamValue,StoreParamsShape,ParamsToIdRecord,ParamsToIdInput. TheuseDataSource*anduseDataSubscribe*hooks anduseArrayChangeIdare deleted; the rest is internal.
Changed
- A store that throws throws in its readers. Its hook is still disabled on its own and every other store keeps running; in 1.x its readers kept its last values, now the components reading it, through
useStore(proxy orselect) oruseMultipleStore, throw its error during render, for their own error boundary. A reader mounted after the failure throws at once, and a store whose hook reads the failed one fails with its error. Once the readers are gone the instance is torn down, and a reader that comes back (an error boundary's retry) starts a fresh one. Put error boundaries around the parts of the screen that can fail on their own: without one, the error reaches the root. The trading demo shows it with a boundary per panel. - A store that moves to an
AutoRootCtxin another React root (two islands, each with its own) keeps running. A check against a store mounted twice disabled it during the move, which in 2.0 would make its readers throw; the check is gone, since oneAutoRootCtxruns one instance per name. StoreState<State>andStoreRef<State>take the state type only.mockStore's hook receives(params)only, and the testing helpers takeuseStoreorstoreRef.- In development, the 1.x forms that JavaScript would accept silently throw an error that says what to write instead: a selector function passed to
useStore,initialStateorAttachedComponentin the options, a number as options, a fourth argument. - A Vite production app that uses
createStoreandAutoRootCtxonly shrinks from 19.4 to 14.6 kB minified (7.7 to 5.9 kB gzip). The trading demo's script time per second stays within the noise of 1.10 at 20× and 50× feed speed, at 60 fps (headless Chrome, production builds, three alternating runs). - The docs drop the pages and sections of removed APIs; Error handling describes the readers that throw.
1.10.0 - 2026-10-06
Added
- The 2.0 API, next to the 1.x one. 2.0 keeps the idea (stores that are lazy, shared, automatic and composable) and trims the API to
createStore, which returnsuseStoreandstoreRef, plususeMultipleStoreand<AutoRootCtx />. Move to it on 1.10, then upgrade; the docs have a Migrating to 2.0 page. storeRef(params): one instance of a store, for code outside React and foruseMultipleStore. It is the handlegetStore(params)returned (get(),subscribe(),retain(),ready,error), under its new name;getStorereturns the same and is deprecated.useMultipleStore(refs, options?): reads several store instances in one call,useMultipleStore(ids.map(id => taskRef({ id }))). The refs may come from one store or several, and their number may change between renders, which the rules of hooks did not allow withuseStore. Each instance starts, is shared and stops as withuseStore, in components and inside store hooks. It returns one tracking proxy per ref, typed by position for a tuple, or with{ select, isEqual }one value over all the states (shallowEqualby default).scheduleapplies to every instance; by default each follows its store's. On the server every state is{}, and while hydrating it reads what the server rendered, asuseStoredoes.useStore(params, { select, isEqual, schedule }): the selector as an option. ItsisEqualdefaults toshallowEqual, so a selector that returns a fresh array or object with the same items does not re-render.react-state-custom/schedulers:sync,frame,throttle,debounce,idle,scheduled,useFrameStateand theScheduler/ScheduledTasktypes, sharing their queues with the main entry.- Types
StoreRef,StoreSelect,StatesOfandStore(whatcreateStorereturns). - The playground's Scoped State example became Many Instances: counters by id, all of them read with
useMultipleStore, and their total withselect.
Deprecated
Marked @deprecated, so editors strike them through; they work as before in 1.x and are removed in 2.0:
initialState(default at the read:count ?? 0; on the serveruseStorereturns{}),AttachedComponent(an effect in the store hook), a number or a fourth argument tocreateStore, and thepreStateargument of the store hook.useStore(params, selector, isEqual)anduseStore(selector): useuseStore(params, { select, isEqual }).getStore(renamedstoreRef),useStoreSuspense(render the fallback from the store's values),useStoreStatus(loading state in the store; from 2.0 a store that throws throws in the components reading it),useCtxState.StateScopeProvider(put what tells instances apart in the params; remountAutoRootCtxwith a newkeyto start every store afresh),StoreErrorBoundaryand theWrapperanddebuggingprops ofAutoRootCtx(each store keeps its own error boundary; report with React'sonCaughtError).createRootCtx,createAutoCtx, theContextclass and its hooks (getContext,acquireContext,useDataContext,useDataSource*,useDataSubscribe*,useDataSelector,useQuickSubscribe),useArrayChangeId,paramsToId,shallowEqual,formatStateand their types. They become internal.- Importing the schedulers,
scheduledanduseFrameStatefrom the main entry: import them fromreact-state-custom/schedulers.
Changed
- Docs, README, AI context, demos and examples use the 2.0 API only. The trading demo has no
initialStateleft: readers default at the read, and the panels that need a symbol render once the workspace has published it. Its script time per second stayed within the noise of before (headless Chrome, production build, three alternating runs). - A Vite production app that uses
createStoreandAutoRootCtxonly grows by about 0.9 kB minified, 0.3 kB gzipped (18.5 → 19.4 kB, 7.4 → 7.7 kB), mostly theshallowEqualthatselectcompares with. It still ships no scheduler and nouseMultipleStorecode.
1.9.0 - 2026-10-05
Added
- A
schedulefor readers: when a component re-renders for a change of the store it reads.useStore(params, { schedule })anduseStore(params, selector, { isEqual, schedule })take a scheduler from a factory exported by the package:frame()(once per animation frame),throttle(ms)(at once, then at most once per period with the last change),debounce(ms, { maxWait })(once changes stop, and at least everymaxWait, 1000 ms by default),idle(ms)(when the browser is idle, at mostmslater) orsync()(at once, the default). Only the factories an app imports end up in its bundle, and each returns the same scheduler for the same arguments, so it can be called inline. A scheduler is an object with atask(run)method, so an app can write its own.createStore(name, useFn, { schedule })sets the default for every reader of a store,useStoreSuspenseincluded. Only the reader's re-render waits: the store,getStore(), actions and the other readers see a change at once, and the render reads the data of its moment, so a change undone meanwhile renders nothing. A store's first data is never held back. Readers due at the same moment (one frame, one throttle tick, one idle callback) render in one React commit. For views that follow fast data without needing every value, such as charts, logs and totals; see Update cadence in the docs. In the trading demo, throttling the depth chart and equity and rendering history and fills when idle took the depth chart from 60 to 10 commits per second and script time down by about 5% at 20× and 50× feed speed (headless Chrome, production build); the order book ladder, which stays on every frame, is most of that app's work. useFrameState(initial): auseStatewhose updates apply once per animation frame, in order, with one render. In a store fed by a socket, the store renders and publishes once per frame instead of once per message, in the same frame as its readers onframe().scheduled(fn, scheduler):fnon a scheduler, for code outside render, such as a socket handler that collects messages in a buffer and publishes once per frame. The calls made before a run are one run, with the last arguments;cancel()andflush()on the returned function.flushScheduled()inreact-state-custom/testingruns every pending scheduled render,useFrameStateupdate andscheduledcall.resetStores()also forgets pending ones.useStore(selector, isEqual | options?)for a store without required params. It tookuseStore(undefined, selector); the old form still works. A call that passes read options as the params of such a store,useStore({ schedule: frame() }), throws a development error that says so.- Exports the types
Scheduler,ScheduledTask,StoreReadOptionsandStoreSelectOptions. - Docs: new pages Update cadence, Organizing stores in layers (domain, core stores that own IO, UI stores and hooks, views; with an import-direction test), Events from a store (a listener API on the store and a notifier store in the UI layer, instead of a "last event" key or a store calling the UI) and Realtime data (snapshot and sequenced deltas, gaps, reconnects, history merged with the live stream). The testing guide shows how to fake a socket and API module to reproduce races, and how to test scheduled readers. The README compares with RTK Query, and notes that Jotai 3 moved
atomFamilyto thejotai-familypackage. demos/trading: a realtime trading terminal built on the library (yarn demo:trading): a simulated exchange of 200 markets sending up to ~1,100 messages a second, an order book with sequence gaps and resyncs, candles, an order ticket validated against the book and balances, optimistic orders, stores in layers with tests per layer.
Fixed
timeToClean: Infinitytore the instance down as soon as its last reader left instead of keeping it: a timer longer than 2³¹ − 1 ms runs at once. AtimeToCleanthat long or longer now keeps the instance untilAutoRootCtxunmounts.useDataSubscribe(ctx, key, ms)anduseDataSubscribeMultipleWithDebouncenever rendered a key that changed more often than the debounce time: each change restarted the wait. They now render at least once a second (or everyms, when longer) while changes go on. Their last argument also takes a scheduler.- Docs: the advice to list an action in an effect's dependencies so the effect runs again once the action exists. That holds for an effect that only calls the action. An effect that does other work, such as opening a socket whose messages call an action from another store, closed and reopened its socket when the action arrived, or not, depending on whether the other store was already running. Getting started now says which effects should list an action, and to read the others through
useEffectEvent, a ref, or an event.
Changed
- The library in a Vite production app that uses
createStoreandAutoRootCtxonly grows by about 0.9 kB minified, 0.4 kB gzipped (17.6 → 18.5 kB, 7.0 → 7.4 kB). Each scheduler imported adds its own code:throttleabout 1.1 kB minified; all of them withscheduledanduseFrameState, 22.5 kB minified, 8.8 kB gzipped in all.
1.8.0 - 2026-10-05
Added
react-state-custom/testing, an entry with helpers for tests. Each takes a store by any functioncreateStorereturned for it, so a module that exports onlyuseStoreis enough, and is typed from it.mockStore(useStore, values)makes the instances that start afterwards publishvalues(some keys or all, checked against the store's type) instead of running the store's hook;valuescan also be a hook of(params, preState), which may use hooks or throw to fail the store. The store'sAttachedComponentdoes not run.set(values)on the returned handle merges new values in and re-renders the readers;restore()stops mocking. Mocks apply in every scope. A component test no longer needs the network or the stores behind the one it reads.resetStores()removes the mocks and drops every cached context with its state, for anafterEach. Tests used to cleargetContext.cache, an internal detail.storeHandle(useStore, params)returns thegetStore(params)handle, also for a store whose module does not exportgetStore; reaching one used to takegetContext('name?params')and the internal instance name.waitForStore(useStore, params, keys | isReady?, { timeout }?)resolves with the state once the store has published, the keys hold values (typed as present) or the predicate holds; it rejects with the store's error if it fails, and after the timeout (1000 ms) with the reason. While it waits React's act environment is off, as in Testing Library'swaitFor, so the store's own updates log no act warnings.
Fixed
- Types under TypeScript's
moduleResolution: "node16"or"nodenext". The declarations imported each other without file extensions, which that resolution rejects in ES modules, so withskipLibCheck(the usual setting) every type of the library was silentlyany; and a CommonJS file importing the package got error TS1479 on every import, because therequirecondition shared the ESM declarations of a"type": "module"package. Declarations now import with extensions, and each has a CommonJS twin (.d.cts) that therequireconditions ofexportspoint to.bundlerandnode10resolution were not affected and read the same.d.tsfiles as before. CI now type-checks an app importing the built package under each resolution, as ES module and as CommonJS, withskipLibCheckoff. - Under
node10resolution withoutesModuleInterop, the declarations' default import of React was an error; withskipLibCheckit was hidden and the React types in them resolved toany, so the dev tool's components were typedany. The library now imports React as a namespace.
1.7.0 - 2026-10-05
Added
shallowEqual(a, b), for theisEqualargument ofuseStore(params, selector, isEqual):Object.isone level deep over arrays, plain objects,Maps andSets, so a selector that returns a fresh array with the same items does not re-render its component. The docs used it without the library exporting one.
Fixed
- A torn-down store instance could still run. Its context stayed cached for 100 ms with everything it had published, actions included, and a component that mounted in that window, or that restored the context (inside an
<Activity>shown again), got the dead instance's actions. Calling one from an effect, as inuseEffect(() => { load?.() }, [load]), ran the dead instance's closure: its fetch, its subscription. The effect then ran again for the new instance, so the work was done twice. Now the actions of a hook that stopped running do nothing, and when the instance is torn down they leave its context, so such a component reads them asundefined, as before any instance ran. The same applies to the actions of a store that threw: calling them no longer runs the failed hook's closures. initialStatetyped every key of the store as present, actions included, when it held a string literal withoutas const({ status: 'loading' }): TypeScript widened'loading'tostring, the object no longer matched the store's type, and the result fell back to "every key seeded". A consumer could then call an action or readids.lengthwith no check and crash on the first render.createStoreandcreateAutoCtxnow infer only the keys ofinitialStateand check each value against the hook's type:{ status: 'loading' }needs no cast, only the keys it holds are typed as present, and a misspelled key or a value the hook never returns is a type error. Code that compiled only because of the old typing gets type errors at those reads, which were unchecked. As before, a string literal returned from the function form needsas const. A seed with keys the store's type lacks, such as a record spread next to named keys, needs the hook's return type to keep the record's index signature (see Collections under Selectors in the docs).
Changed
- When a store instance is torn down (
timeToCleanafter its last reader) and nothing holds its context, the context is dropped at once instead of 100 ms later. A component mounting in that window starts a fresh instance frominitialState, instead of rendering the old instance's values once. That 100 ms grace predatestimeToClean; keep state across a remount withtimeToClean.preStatestill warm-starts an instance whose context something holds: a restart after an error or a hot update, an<Activity>shown again, agetStore().subscribelistener.getStore().readyanduseStoreStatus().readyturn false once the instance is torn down. In the task-manager demo, coming back to a project right after leaving it no longer renders a row whose task isundefined. - Docs: values from different stores can disagree for one render, and readers are written to tolerate it rather than avoid it. How it works describes stores as services that own their data, IO and lifetime, and a "Data across stores" section: check before reading (with
noUncheckedIndexedAccesssotasks[id]is typedTask | undefined), join by id rather than by position, and keep decisions that need several stores in one store. Getting started gains "Before the data arrives" (data and actions areundefineduntil the store runs; call actions with?.()from handlers, and list them as effect dependencies). The Collections section shows rows that take an id, read their item with a selector and check for a missing one, and two shapes for items with their own fetch: a store per id, or one collection store with asubscribe(id)action that counts readers.
1.6.0 - 2026-10-04
Performance
- An update that changes many keys of one store, such as a collection kept as an object keyed by id, costs time linear in the keys. A store update notifies its subscribers once per changed key, and the
useStore()proxy,useDataSubscribeMultipleandgetStore().subscriberedid their work on each notification: the proxy compared the keys it had read up to the first changed one (or listed every key, forObject.keys(store)), the multi-key hook re-read every key, and the handle copied the full state for each call. Mounting a component that reads many keys that already hold values was quadratic for the same reason, as subscribing reports each present key at once. Each now does its work once per update. With 4000 keys in jsdom, every key changing: a component listing them ~900 ms → ~2.5 ms,useDataSubscribeMultiple~362 ms → ~2.6 ms, onegetStore().subscribelistener ~2.6 s → ~3.0 ms. The last 2000 changing, a component reading all 4000: ~48 ms → ~1.9 ms (when every key changes, its check stops at the first one, so that case was already fast). Mounting and unmounting a component that reads all 4000: ~199 ms → ~1.5 ms. - Starting a store instance no longer captures a JavaScript stack, which it did in production too, for an error thrown only when one instance is mounted twice. That was about a third of the cost of starting an instance: mounting 5000 instances at once ~440 ms → ~260–320 ms (jsdom). The error is unchanged; the error boundary reports where it happened with React's component stack, and the captured stack only ever pointed into React and the library.
- Development-only code is removed from production bundles built with Vite or Rollup, or minified with terser as webpack does. Every check read one
isProductionconstant computed inside atry/catch, which minifiers cannot fold, so the development warnings, their text, the read-only proxy traps and the dependency graph all shipped to production. The constant is now plainprocess.env.NODE_ENV === 'production': the app's bundler replaces the expression and the minifier drops the branches. The library in a Vite production app: ~22.1 kB → ~17.4 kB minified, ~8.8 kB → ~7.0 kB gzipped. Bundling with esbuild alone keeps most of them, as before. It is read once, not at each check: in Node (tests, server rendering of unbundled packages) aprocess.envread costs about 150 ns, which at every check would make updates 6–16% slower in the jsdom benchmarks.
Fixed
- Two React roots hydrated from server HTML (islands), each with a
StateScopeProvider, get separate scopes. The scope id came fromuseId, which numbers a hydrated root by tree position, so both scopes had the same id and shared their stores: twoAutoRootCtxran in one scope, the store mounted twice was disabled, and the islands stopped responding. Scope ids are now unique in the page. Roots created withcreateRootwere not affected.
Changed
- Like React, the library needs
process.env.NODE_ENVto be replaced by a bundler, or defined by the CDN serving it (esm.sh does). A runtime fallback letdist/index.es.jsload in a browser with no bundler at all; there, importing it now throwsprocess is not defined. getStore().subscribepasses the same snapshot object to every call of one update (one call per changed key, as before) instead of a fresh copy for each. Treat it as read-only.
1.5.1 - 2026-10-04
Fixed
useStoreSuspense(params, keys)types listed keys as present only whenkeysis a tuple: a list written in the call or keptas const. A widened array such asreadonly ('items' | 'add')[]holding only'items'typedaddas present too, although the component waited foritemsalone andaddcould beundefined. Such an array is now typed likeuseStore; what it waits for is unchanged.
1.5.0 - 2026-10-04
Added
useStoreSuspense(params, keys): a list of keys in place of the predicate waits until each holds a value (seeded or published,nullincluded) and types those keys as present, the others as optional. A predicate can hold oninitialStatebefore the store runs, so that render reads only the seeded keys although the result is typed as the full state; the key list keeps the type to what the render gets, and still renders at once when the seed holds every key. A development warning names a listed key the store later sets back toundefined. Exports theStoreStateWithtype.useStoreStatus(params?), returned bycreateStore:{ ready, failed, error }for the instance, re-rendering only when that changes. A store hook that crashed leftuseStoreconsumers on its last values, so one that failed before its first result kept showinginitialState(a spinner that never stopped) with no way to tell. It works in aStateScopeProvider, unlikegetStore().error, and inside another store, which can publish the failure of a store it reads.
1.4.0 - 2026-10-03
Added
- A development warning when a
useStoreSuspenserender resolved by a predicate that held oninitialStateread a key the seed does not have. That render comes before the store runs, so the key wasundefinedalthough the result is typed as the full state; the warning names the key once the store has published a value for it. Rendering from the seed is kept: waiting for the store instead shows the fallback and delays the content by React's reveal throttle (about 300 ms).
Fixed
- A selector (
useStore(params, selector),useDataSelector) no longer misses a change made between its component's render and its subscription. It recomputed only after its own listener had run, and React subscribes after the layout effects of the commit, so a store publishing in that commit left the component showing the old value until the next change. An event that both shows a component reading a selector and changes the store did this. Selectors now recompute after any change of the context, which counts them inContext.revision. useStoreSuspenseconsumers of one instance wait for their own predicates. The instance had one pending wait holding the predicate of the last component that suspended, so withs => s.ain one boundary ands => s.bin another, the first kept its fallback untilbwas ready too, and for good if the second component went away. The wait now resolves as soon as one predicate holds, and the components still not ready wait again.- A component that goes away while
useStoreSuspenseis suspended no longer keeps its store running until the store gets ready. A suspended component never commits, so nothing released the retain it took; with a predicate the store never satisfied, the store ran, fetches and subscriptions included, until the page closed. The wait now wakes its components every 5 seconds: those still there wait again on the same instance, and the store of one that went away is released about a second later. Object.keys(useStore()), a spread,for...inand'key' in storefollow keys being added and removed. Listing the keys subscribed to nothing and a spread only to the keys present at that render, so a list of a keyed collection never showed new items, and with a lazy store never showed any. Listing the keys now re-renders on additions and removals only; the development warning is kept for spreads, which read every value.- A store named like an
Object.prototypemember (constructor,toString,valueOf, ...) starts. AutoRootCtx checked its instances withkey in records, which counted inherited members, so such a store without params never mounted, and__proto__replaced the prototype instead of adding a key. - The selector form of
useStorerequires the params a store requires.useStore(undefined, selector)type-checked for a store whose hook needs{ id }, and the hook ran with{}. <DevToolContainer defaultOpen />renders on the server. It readwindow.innerHeightduring render and threw "window is not defined". The panel's open state and sizes remembered by the tab are now applied after mounting, so hydration also sees the markup the server sent.
1.3.2 - 2026-10-03
Fixed
- A component reading the
useStore()proxy keeps updating while a transition that re-renders it is suspended. The proxy recorded the keys of each render as it ran, so a render React then put aside (a transition waiting on a suspended sibling keeps the previous UI on screen) replaced the keys of the UI on screen: a component switching froms.atos.bin that transition stopped followingaand showed a stale value until the transition finished. Changes are now checked against the keys of the committed render. - Selectors (
useStore(params, selector),useDataSelector) anduseDataSubscribeWithTransformcheck changes with the selector of the committed render. They read the selector of the latest render, so in the same situation a component whose put-aside render selected a value equal to the one on screen (switching froms => s.atos => s.bwhilebequalleda) missed the next change ofa.
1.3.1 - 2026-10-03
Fixed
- A store context created by a render that never commits is no longer cached forever. Components get their context during render, and only a committed component counts as a user, so a render that threw, suspended or was discarded left the context, and its dependency-graph entry in development, in the cache for good. A context created during render is now evicted after a second unless a user has committed; a render that commits later still finds or restores it.
- The
useStore()proxy warns about spreading only in development, and once per store. The warning was logged in production builds too, on every render of every component that spread the proxy. - Writing to the
useStore()proxy throws aTypeErrorin development.store.count = 1wrote straight into the shared data without notifying anyone, so other readers kept the old value until the store published again. Production builds are unchanged, as React only freezes props in development. - A
useStorecall site that passes a selector on some renders only (useStore(p, cond ? select : undefined)) gets an error naming the cause in development. The proxy and the selector form run different hooks, so React failed with an unrelated message ("Cannot create property ..."). TypeScript already rejects an optional selector; this covers JavaScript and non-null assertions. - A store whose hook gains, loses or reorders hooks in a hot update restarts instead of being disabled. The running instance ran the new hook on the old hook state, React threw ("change in the order of Hooks") and the store stayed off until every consumer unmounted, so the page needed a reload. It now remounts once with the new hook, warm-started from
preState; an edit that keeps the hooks still keeps the state.
Changed
- React DevTools names every store after itself: the component running the hook of store
todosisStore(todos), so the DevTools search finds it. Every store was aStateRunner, and in the published package, which is minified, the other components showed mangled or empty names;AutoRootCtx,Bucket,StoreInstance,StoreErrorBoundary,StoreFailureandStateScopeProvidernow have display names. The hot-reload boundary is merged intoStoreFailure, one level less per store. - CI runs the test suite on React 18.3 too. The peer range has always been
react >= 18, but only React 19 was tested. Three tests relied on React 19 behavior (use, StrictMode reusing the first render's state) and now pass on both; the library needed no change.
Performance
AutoRootCtxrenders only the buckets that hold a running instance instead of always 64, so an app with a few stores mounts a fewBucketcomponents. MountingAutoRootCtxwith 3 stores: ~1.2 ms → ~0.6 ms. Mounting 1000 and 3000 instances at once: ~72 → ~67 ms and ~259 → ~210 ms; starting or stopping one instance is unchanged (jsdom). Inyarn bench, updates that re-render no consumer are 31–49% faster than in 1.3.0 (shoptheme: ~0.046 → ~0.024 ms), back to their level before the 64 buckets of 1.2.6; the other scenarios are unchanged within noise.
1.3.0 - 2026-10-03
Fixed
- Functions returned by a store reach the consumers that call them while rendering. Every function was published as one stable wrapper, so a getter such as
useCallback(id => items[id], [items]), a sort order kept inuseState, or a component returned by the store never updated its consumers: they kept rendering the old result. A consumer that calls a store function during render (directly, through a selector, or in a memoized child it passes the function to) now re-renders when the store returns a new implementation, and a returned component remounts when it is replaced. Actions called from event handlers keep a stable identity and never re-render their readers. - A store hook that suspends (
use(promise), a suspense query hook) no longer hides the whole app. Store hooks ran without a Suspense boundary of their own, so one suspending store showed the fallback of the boundary aboveAutoRootCtx, often the app's root. Each store and eachAttachedComponentnow has its own<Suspense fallback={null}>: the store publishes once it resumes, keeps its last values while it suspends again, anduseStoreSuspenseconsumers wait in their own boundaries. - A store hook that throws now reaches
useStoreSuspenseconsumers: they throw its error into their own error boundary. A store that crashed before its first result left them on the Suspense fallback forever, and one that crashed later was invisible to them. The error still reaches theWrapperpassed toAutoRootCtx. - Stores no longer stop for good when a second
AutoRootCtxin the same scope unmounts. The newest root took over the stores but left itssubscribefunction published after unmounting, so every store moved to it was dead and new consumers attached to the unmounted root without any warning. The previous root now takes over again, and with no root left new consumers get the "no AutoRootCtx" error. - Hydration no longer mismatches when a store has already published on the client. Consumers read the live store data during hydration, so an island, or a Suspense boundary of a streamed page, hydrating after the store had run for an earlier one rendered the live values against server HTML made from
initialState, and React threw away the server HTML ("Hydration failed"). While hydrating,useStore, selectors anduseStoreSuspensenow render the store'sinitialStatewhen the live data differs from it, then update. A component hydrating before its store ran renders no extra time. getStore().subscribeandsubscribeAlllisteners no longer see half-applied updates. A store published its keys one by one and notified after each, so a listener sawpricenew next tototalstill old. A store now publishes all changed keys of a render first, then notifies once per key with the complete state.useStoreSuspensesuspends only on first load. After it had resolved, theisReadypredicate was re-checked only when a key the component read changed at the same time: a refetch settingisLoadingalone left the content, the same refetch together with new data replaced it with the fallback. Once a component has rendered an instance ready it now never suspends on it again; new params still do.- TypeScript: store state and params can be declared with
interface. The constraints wereRecord<string, unknown>andRecord<string, primitive>, which interfaces do not satisfy (they have no index signature), socreateStore('x', (p: Params): State => ...)failed with "Property ... is missing in type 'ParamsToIdRecord'". State is now any object type, and params any object type whose values are primitives (StoreParamsShape); object-valued params are still rejected. - The published package is no longer compiled by the React Compiler. Since 1.0.27 the library build ran
babel-plugin-react-compiler, which was meant for the examples only, so the package differed from the tested source. In 1.2.2 to 1.2.6 the compiler cached theuseStore()proxy, undoing the 1.2.2 fix: in an app using the React Compiler, a helper called with the whole store object (describe(store)) kept its cached result and stopped updating. Tests and benchmarks always ran on the uncompiled source, which is now what ships.
Added
- A development error when a second
AutoRootCtxmounts in the same scope. - A development error when the
Wrapperpassed toAutoRootCtxorStateScopeProviderchanges identity, which happens when it is written inline: every store under it is remounted and loses its state. Likewise for adebuggingrenderer, which makes every store hook run again. The error handling guide and API pages showed the inline form; they now define the Wrapper at module scope. - Types
StoreParamsShape<Params>andParamValue. Context.publishMany(entries, removed?): publish several keys as one update.Context.touch(keys): notify subscribers of keys whose value is unchanged (a store function with a new implementation).useQuickSubscribe(ctx, serverData?)anduseDataSelector(ctx, selector, isEqual?, serverData?): what the server rendered, used while hydrating.getStore(params).error: what the store hook threw while the instance is disabled,undefinedwhile it runs.Contextgainsfailed,error,fail(),recover()andonStatus().
Performance
- A store render costs less per key. The hook's result was copied for
preStateon every render, copied again to wrap functions, turned into an entries array, flattened and compared, then published key by key. It is now published in one pass at commit,preStateis read once on mount, and onlyfunctions and classes have their source text checked. Updating one key of a store with 1000 keys and one consumer: ~0.21 ms → ~0.08 ms; with 10 000 keys: ~7.2 ms → ~3.7 ms, of which ~2.6 ms is the hook's own object spreads (jsdom). The standard benchmarks are unchanged within noise.
Changed
preStateis read once when the instance mounts instead of being re-copied on every render.- A store function wrapper forwards to the implementation of the latest committed render, not the latest render: a render React discards no longer leaks its closures into the wrapper.
- CI type-checks the library, the new type-level tests (
tests/types) and the benchmarks (yarn typecheck); it ran no type check before. - CI builds the package and runs the React Compiler tests against
dist/(yarn test:dist), and fails if the build contains compiler output. The publish workflow does the same before publishing.
1.2.6 - 2026-10-03
Fixed
- Store identity: a param passed as
undefinedno longer creates a separate instance.useStore({ id, page: undefined })anduseStore({ id })now share one instance, so an optional param passed straight through no longer runs the hook twice. - Store identity: values of different types no longer collide.
{ id: 1 }and{ id: '1' }, ornulland'null', were one instance whose hook saw whichever params mounted first. A string that reads like a number, bigint, boolean ornullis now quoted in the id (id='1') and a bigint ends inn. Ids of ordinary strings and numbers are unchanged; keys shown by the dev tool anddebuggingchange only for those quoted strings.
Performance
AutoRootCtxkeeps reference counts andtimeToCleantimers out of React state. A consumer mounting or unmounting on an instance that is already running no longer re-renders anything: with 5000 instances, ~17.6 ms → ~0.1 ms per consumer in jsdom.- Running instances are spread over 64 buckets, each rendering its own share. Starting or stopping an instance re-renders only its bucket instead of a list of every instance (React diffs a component's whole child list): with 5000 instances, ~18 ms → ~0.9 ms per instance, and mounting 5000 instances at once ~2.1 s → ~0.4 s in jsdom.
Changed
- SSR guide: says plainly that a store cannot be seeded from server data, and lists what works instead.
- Docs on update cost: stores commit before their consumers, every component renders once per change (StrictMode doubles render calls, not commits), and frame time is decided by the DOM, not the store.
1.2.5 - 2026-10-03
Changed
- npm keywords describe the library (global state, store, derived state, Suspense) instead of generic terms.
1.2.4 - 2026-10-03
Fixed
useStoreSuspense: the store retained while a component was suspended was released 100 ms after the promise resolved, but React reveals a resolved boundary 300 ms (React 19) or 500 ms (React 18) after its fallback. The store was torn down before the component committed, so the commit mounted a fresh instance, refetched and showed the fallback again. The retain now lasts until a component reading the store commits, with a one-second safety release for renders that never commit. Tests run on React's real scheduler, whichact()hid.
Added
- A development error when two different stores share a name. They shared one instance silently, with only one of the hooks running.
Changed
- README, docs home and introduction lead with sharing: a hook written once runs once per params for every caller, composes with other stores and renders progressively. The 30-second example is now a ticker store and a position store reading it. The npm description follows.
- README: the comparison section now sets the library next to Jotai with the same order-book example (hook versus atom), a table of what differs, and one paragraph on Zustand, Redux and React context.
- Docs: new guide page Progressive data (one store per fetched source, a combining store the view renders from, switching params,
timeToCleanas a cache, use with TanStack Query). Limitations now state that layers are one commit apart, that store hooks see the providers aboveAutoRootCtxand that transitions stop at the store; the Suspense page documents switching params instartTransition; Reads outside render notes that a passed proxy is tracked only during render.
1.2.3 - 2026-10-03
Fixed
- Dev tool: the inspector panel and its toggle button now have a
z-index(--rs-z-index, default 9999), so page content no longer paints over them when the page scrolls. The store list hides the internalauto-ctxentry of scoped roots too, not only the global one. - Dev tool:
react-state-custom/style.cssno longer setscolor-schemeon:root(it flipped the host page's form controls and scrollbars to dark on a dark OS); the scheme and the text color are scoped to the panel, which also sets its own text color so it stays readable on dark host pages. The full-viewport overlay that stayed mounted while the panel was closed, with an invisible split bar still catching clicks along the bottom edge, is gone: the panel is only mounted while open. - Dev tool: the store list updates from a cache subscription instead of polling
cache.sizeevery 50 ms, so a store replaced by another in the same tick no longer leaves a stale list. A selected store that unmounts is markedunmountedinstead of silently showing old data, and the view follows the new instance when the store mounts again. debuggingonAutoRootCtx/StateScopeProviderwas documented but rendered nothing. It now renders each store instance's state as<pre data-store="<store>?<params>">, or through a custom component (debugging={MyRenderer}).
Added
react-state-custom/dev-tool/obj-view:ObjectDataView, a dev tool renderer showing values as an expandable tree with react-obj-view, now an optional peer dependency.DevToolContainergainsdefaultOpenanddefaultHeight, typed button props (DevToolContainerProps), a title bar to drag-resize the panel, and remembers its open state and sizes per tab. The list shows decoded params (userId=42), the number of keys, the scope of scoped stores, and each view has a header with params, scope,initial/unmountedbadges and a close button.formatState(value)(main entry): the JSON formatter behinddebuggingand the default dev tool renderer. Keeps functions (ƒ increment()),undefined,bigint, symbols,Map/Setentries and marks circular references instead of throwing.getContext.cache.subscribe(listener): the context cache is now anObservableMapthat notifies after every creation or eviction.Documentation site at https://vothanhdat.github.io/react-state-custom/docs/ (VitePress,
yarn docs:dev/yarn docs:build): a guide split into pages (store options, params, composing, scopes, errors, selectors, Suspense, outside React, dev tools, SSR, React Compiler), new Testing and Limitations/FAQ pages, the API reference split per export, benchmarks and this changelog, with local search. Deployed next to the demo by the Pages workflow.Benchmarks: four scenarios next to the flat one,
yarn benchruns them all. Derived: a summary computed from 10 keys that 1000 consumers read, updated so that the sum changes or stays the same. Topology: a root config store, 10 derived stores reading one threshold each, 1000 consumers; one threshold, every threshold, or a root key no derived store reads changes. Shop: config and items feeding 100 line stores, 10 checkouts and a summary, updated at an item's qty,vat,themeordiscount. Collection: 200 items with 5 readers each, as top-level keys and as an array under one key. The report prints consumer renders, derive calls and the code size (tokens) of each adapter;bench/README.mdhas the tables and what they show (derivations computed once per update, no work at all for keys nobody reads, one extra commit per derived layer, Jotai fastest on time, Zustand and Jotai the least code).Demo: Composed Stores gains a summary store that reads both invoice stores (settings → invoice → summary).
Demo: "Selectors and Suspense" (render counters show which consumers a selector spares;
useStoreSuspensewith a boundary) and "Outside React" (getStore()withretain(),get()andsubscribe()from plain module code) examples. A "Dev tools" toggle in the header (off by default) shows the store inspector inside every example.
Changed
- Dev tool:
@uiw/react-splitreplaced by a small pointer-capture resize hook;dev-tool.es.jsno longer bundles it andstyle.cssdrops its rules. The compare columns share the space equally instead of being individually resizable.DevToolState'sComponentprop is now optional (defaults toDataViewDefault, likeStateView). - Demo: Form and Cart examples removed (same parameterised-instances pattern as Todo and Timer). Example views use a small shared stylesheet instead of inline styles, and the explanatory paragraph that duplicated each example's description is gone.
- README reduced to the pitch, quick start and links into the site.
API_DOCUMENTATION.mdis now a redirect; the reference sources aredocs/api/*.md. AI_CONTEXT examples are typed and useinitialState. - Docs: a Collections section (items as an object keyed by id, not an array under one key), a "Many instances at once" section on composing stores without calling hooks in loops and on the commit cost of deep chains, and
initialStatepresented as an option over the lazy default (read with??/?.) in the guide, FAQ, AI_CONTEXT and Copilot notes. The README comparison table says what "boilerplate" means. - Docs fixes:
markReady/onReadydocumented onContext, the unusedtoggleButtonprop dropped from the dev-tool reference, snippets fixed to type-check (usernarrowing, optionalitems), spreading theuseStoreproxy documented as subscribing to every key.
1.2.2 - 2026-10-02
Fixed
- React Compiler:
useStore()/useQuickSubscribereturn a new proxy on every render. The compiler memoises work on the identity of its inputs, so with the previous long-lived proxy a helper called with the whole store object (describe(store)) kept its cached result and its reads stopped being tracked; the component never updated. Compiled components that destructure keys were unaffected.
Added
yarn test:compiler: the store APIs tested underbabel-plugin-react-compiler(components and store hooks compiled), run in CI next to the main suite.yarn bench: microbenchmarks against Zustand, Jotai and a plain React context (update cost, mount cost, consumer renders per update) with results and caveats inbench/README.mdand a summary in the README.
1.2.1 - 2026-10-02
Changed
Contextnotifies subscribers directly instead of throughEventTarget.dispatchEvent. The browser (and jsdom) report a listener's exception towindow.onerrorand carry on, which hid React's "Maximum update depth exceeded" from the publisher: whether a divergent store cycle stopped depended on which subscriber happened to be notified first. Now every subscriber is still notified and the first error is rethrown to the publisher, so the error lands in the publishing store's effect andStoreErrorBoundarydisables that store with a logged error, as a throwing subscriber surfaces atdispatchin Redux orsetStatein Zustand. Listeners passed tosubscribe,subscribeAllorgetStore().subscribeshould therefore not throw.- A consumer that mounts in the same pass as
AutoRootCtx(the usual app-root case) now renders twice on the way to its first data instead of three times, and a consumer whoseinitialStatealready matches the first publish renders once.useCtxStateno longer subscribes to theauto-ctx"subscribe" function during render; it picks it up inside its effect, which also re-attaches the store ifAutoRootCtxis replaced. Thetests/render-count.test.tsxsuite pins these numbers.
1.2.0 - 2026-10-02
Added
getStore(params): an imperative handle for code outside React.get()returns a plain snapshot,subscribe(listener)delivers every change with the changed key,retain()keeps the store running without any component (returns a release function),readytells whether the hook has published. Global scope only.useStore(params, selector, isEqual?): re-render only when the selected (possibly deep or derived) value changes. Backed by the newuseDataSelector(ctx, selector, isEqual?)hook.useStoreSuspense(params, isReady?): suspend until the store hook has published once, or untilisReady(state)holds; returns the full state type. The store is retained while the component is suspended.Context.ready/markReady()/onReady()andacquireContext(name)(ref-counted, non-hook access to a context) for building such integrations.
1.1.2 - 2026-10-02
Changed
- Demo: examples now run natively in the playground next to their source, with an "Edit on StackBlitz" button that opens a new tab. StackBlitz embeds no longer start on pages that are not cross-origin isolated, and isolating the page blocks StackBlitz's own relay frame (stackblitz/sdk#37, stackblitz/webcontainer-core#2045).
1.1.1 - 2026-10-02
Changed
- Docs: README gains "How It Works", a consumer step in Quick Start and a guide for store options, params, composed stores, error handling and reads outside render. API reference now covers every export, including
StateScopeProvider,StoreErrorBoundary, theStore*types and the low-level hooks. - Demo: playground snippets and examples use
createStorewith an options object andinitialState; new async-data, composed-stores and scoped-state examples; "React 19" wording replaced by React 18+.
1.1.0 - 2026-10-02
Fixed
AutoRootCtxno longer moves a store's record to the end of its state object on unsubscribe. The move re-ordered keyed children, and React StrictMode re-runs effects of re-placed fibers, which made consumers unsubscribe/resubscribe and re-order again: an infinite loop in development with two or more stores. Records keep their position and stores render in a stable sorted order.- A divergent store cycle (
A = B + 1,B = A + 1) now runs synchronously and is capped by React's own nested-update limit (reported throughreportError), instead of spinning forever in the background through timers. - Action functions returned from a store hook now keep a stable identity across store renders (each function key gets one wrapper that forwards to the latest closure). Consumers that destructure actions no longer re-render on unrelated key changes.
- All subscribe hooks (
useQuickSubscribe,useDataSubscribe,useDataSubscribeWithTransform,useDataSubscribeMultiple,useDataSubscribeMultipleWithDebounce) are now built onuseSyncExternalStore. Updates are delivered synchronously, in one commit, with no tearing between hooks and no straysetTimeoutin render. - Change detection uses
Object.iseverywhere; publishing0over""ornulloverundefinedis no longer swallowed. useDataContextre-validates its instance on commit, so a context evicted between render and commit is restored (or the live instance adopted) instead of leaving two Contexts for one name. Eviction never deletes a different live instance.paramsToIdURI-encodes keys and values, so=,&and?inside a value can no longer collide with another params object.process.env.NODE_ENVis read inside a try/catch; the ES build no longer throws in environments without aprocessglobal.- Using a store without any
<AutoRootCtx />/<StateScopeProvider>mounted now logs aconsole.error(dev only) after 1s instead of failing silently. - Dev tool:
StateViewreads from the cache instead of creating contexts (no more resurrected stores), grouped list items have keys,Object.groupBy/toSortedreplaced for wider runtime support, typing fixed.
Added
createStore/createAutoCtxaccept an options object{ timeToClean, AttachedComponent, initialState }. A bare number is still accepted astimeToClean.initialStateseeds a store's context before the first consumer render, so values are neverundefinedon first paint. Keys listed ininitialStateare typed as always present on theuseStoreresult.useStore()/useCtxState()can be called without arguments when the store has no required params.- Exported types
StoreOptions,StoreParams,StoreState. - SSR support clarified and tested: on the server
useDataContextuses throwaway contexts instead of the shared cache (no per-request memory growth, no cross-request sharing);renderToStringoutput equals the client's first render so hydration matches. README section on server-side rendering and'use client'note for Next.js. StoreErrorBoundary: the defaultWrapperofAutoRootCtx. A store hook that throws is disabled and logged; every other store keeps running. Pass your ownWrapperto override.- Keys that a store hook stops returning are now published as
undefinedand removed from the context data.
Changed
- Breaking: the dev tool moved to its own entry. Import
DevToolContainerfromreact-state-custom/dev-tooland the stylesheet fromreact-state-custom/style.css. The main entry no longer carries the dev-tool CSS or its UI dependency, and@uiw/react-splitis no longer installed for consumers. - Breaking: the UMD build is replaced by a CommonJS build (
dist/index.cjs).package.jsonnow has anexportsmap andsideEffects. - Stores publish from
useLayoutEffect(client) so consumers see the first values before paint instead of one frame later. StateRunneris memoized: a consumer mounting or unmounting no longer re-runs every other store's hook.useQuickSubscribe(undefined)now returns a proxy whose properties read asundefinedinstead of throwing at creation.- Reading the
useStoreproxy outside render no longer throws. It returns the current value, is not tracked, and logs a one-time development warning. Symbol keys pass through untracked. - Peer dependency relaxed to
react >=18/react-dom >=18(nothing React 19 specific is used). - Tests run under
<StrictMode>.
Removed
- Internal
useRegistryChecker(dead code).Context.registryis kept but deprecated. - The dev-only dependency graph forgets a store when its context is evicted, so it no longer grows unbounded.
1.0.33 - 2026-02-22
- Added
StateScopeProvidercomponent for isolated nested state — allows subtrees to mount their own independent store instance, preventing state leakage between siblings or nested consumers. - Re-exported
StateScopeProviderfrom the package entrypoint (src/index.ts). - Rewrote README and API documentation for clarity and impact; fixed example imports and corrected
AutoRootCtxusage docs. - Added
AI_CONTEXT.mdwith guidelines on preferred patterns and API usage for AI-assisted development.
1.0.32 - 2025-11-30
- Added
createStorehelper function to simplify store creation (combinescreateRootCtxandcreateAutoCtxinto one step). - Added
useStorehook to the return value ofcreateAutoCtxandcreateStorefor easier, proxy-based state consumption. - Added documentation for "Composing Stores" pattern (derived state).
- Added
preStateargument tocreateRootCtxhooks so roots can warm-start from previously published data when they remount (helps AutoRootCtx keep state continuity). - Updated documentation to describe the new
useFn(props, preState)signature and warm-start behavior. - Implemented
DependencyTrackerfor circular dependency detection; warns at runtime when two stores subscribe to each other in a cycle.
1.0.31 - 2025-11-25
- Last tagged release before this changelog was introduced (see git history for details).