{"_id":"@canlooks/statio","_rev":"6-73706fcfc0db61e0767eb7048f38859b","name":"@canlooks/statio","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@canlooks/statio","version":"1.0.0","keywords":["react","state manager"],"author":{"name":"C.CanLiang","email":"canlooks@gmail.com"},"license":"MIT","_id":"@canlooks/statio@1.0.0","maintainers":[{"name":"canlooks","email":"364021661@qq.com"}],"homepage":"https://github.com/canlooks/statio","bugs":{"url":"https://github.com/canlooks/statio/issues","email":"canlooks@gmail.com"},"dist":{"shasum":"a1e7a1bc29ffafdbf51484be075b207808f8bcd8","tarball":"https://registry.npmjs.org/@canlooks/statio/-/statio-1.0.0.tgz","fileCount":18,"integrity":"sha512-ahxGFEDTL1S2U2/EvKqHorloja5NN9JwZFrgMTdYVOGvb42HusyWpEOjk+JmZUK18PMMIEUdKOTCtB88MqRUyA==","signatures":[{"sig":"MEQCIGz3sPIGfYPuG/BqSUeaWQ7Hjf8Ej+lZV7RjO9b81WiMAiBoPWUFlYKe55JjwYnOtiMmIlzPnTBTBjzfs/dRt2IeYA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":41307},"main":"dist/cjs/index.js","types":"index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"3e3197c35378ebf784e3969fafea8a535f0907e9","scripts":{"build":"tsc -m esnext --outDir dist/esm & tsc -m commonjs --outDir dist/cjs","clean":"npx shx rm -rf dist","rebuild":"npm run clean && npm run build && npm run build:alias","test:ssr":"next dev test","test:unit":"npx ts-node test/unit.ts","build:alias":"tsc-alias --outDir dist/esm","test:browser":"vite -c test/vite.config.mts"},"_npmUser":{"name":"canlooks","email":"364021661@qq.com"},"repository":{"url":"git+https://github.com/canlooks/statio.git","type":"git"},"_npmVersion":"11.9.0","description":"Simple react state manager","directories":{},"_nodeVersion":"24.14.0","dependencies":{"tslib":"^2.8.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.2.6","vite":"^8.0.13","react":"^19.2.6","zustand":"^5.0.13","react-dom":"^19.2.6","tsc-alias":"^1.8.17","typescript":"^6.0.3","@types/node":"^25.8.0","@types/react":"^19.2.14","@types/react-dom":"^19.2.3"},"_npmOperationalInternal":{"tmp":"tmp/statio_1.0.0_1779090439877_0.2138177874354985","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@canlooks/statio","version":"1.0.1","keywords":["react","state manager"],"author":{"name":"C.CanLiang","email":"canlooks@gmail.com"},"license":"MIT","_id":"@canlooks/statio@1.0.1","maintainers":[{"name":"canlooks","email":"364021661@qq.com"}],"homepage":"https://github.com/canlooks/statio","bugs":{"url":"https://github.com/canlooks/statio/issues","email":"canlooks@gmail.com"},"dist":{"shasum":"ea03e04301735a79269fe58317fdd004cb73a8ea","tarball":"https://registry.npmjs.org/@canlooks/statio/-/statio-1.0.1.tgz","fileCount":18,"integrity":"sha512-vZ+VsSkn1Tg4jQug9F3WUGoWHhg6WPmQ8yRQbdCThKxLk/N6J42yJXYWOg63qRgRmZ8Bxq2xYHymKzOLeo/mZw==","signatures":[{"sig":"MEUCIQC4q8C3B7ruL5EVa/oQEcgkhB1/ZzBee5XjZqtvJ4zt9QIgDNeLYHq2mrZUS2kXzAtGa19jCYxV49D5iHYE2OfAng4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":40579},"main":"dist/cjs/index.js","types":"index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"3e3197c35378ebf784e3969fafea8a535f0907e9","scripts":{"build":"tsc -m esnext --outDir dist/esm & tsc -m commonjs --outDir dist/cjs","clean":"npx shx rm -rf dist","rebuild":"npm run clean && npm run build && npm run build:alias","test:ssr":"next dev test","test:unit":"npx ts-node test/unit.ts","build:alias":"tsc-alias --outDir dist/esm","test:browser":"vite -c test/vite.config.mts"},"_npmUser":{"name":"canlooks","email":"364021661@qq.com"},"repository":{"url":"git+https://github.com/canlooks/statio.git","type":"git"},"_npmVersion":"11.9.0","description":"Simple react state manager","directories":{},"_nodeVersion":"24.14.0","dependencies":{"tslib":"^2.8.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.2.6","vite":"^8.0.13","react":"^19.2.6","zustand":"^5.0.13","react-dom":"^19.2.6","tsc-alias":"^1.8.17","typescript":"^6.0.3","@types/node":"^25.8.0","@types/react":"^19.2.14","@types/react-dom":"^19.2.3"},"_npmOperationalInternal":{"tmp":"tmp/statio_1.0.1_1779091681160_0.39513571015808124","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@canlooks/statio","version":"1.0.2","keywords":["react","state manager"],"author":{"name":"C.CanLiang","email":"canlooks@gmail.com"},"license":"MIT","_id":"@canlooks/statio@1.0.2","maintainers":[{"name":"canlooks","email":"364021661@qq.com"}],"homepage":"https://github.com/canlooks/statio","bugs":{"url":"https://github.com/canlooks/statio/issues","email":"canlooks@gmail.com"},"dist":{"shasum":"f0c9a21684c5fa1def8fcad9215803cfc140513e","tarball":"https://registry.npmjs.org/@canlooks/statio/-/statio-1.0.2.tgz","fileCount":18,"integrity":"sha512-TZZK7TVg1S+M2nXAtCNor+DCnCF+eGvvI6uxkex6nD8zgDxq1Rv2DZ0kMPM/yk2o3RDq/cnWn19npz38ufPQGw==","signatures":[{"sig":"MEQCIBTSJ892vYNgxXwzjWRtpBvXJQ/QPA2hPW7hD7p3QshWAiBToZoguE3LMMdXNnqPjqxcX6RdKp5yyOTcwJUVqTZxzA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":40695},"main":"dist/cjs/index.js","types":"index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"3e3197c35378ebf784e3969fafea8a535f0907e9","scripts":{"build":"tsc -m esnext --outDir dist/esm & tsc -m commonjs --outDir dist/cjs","clean":"npx shx rm -rf dist","rebuild":"npm run clean && npm run build && npm run build:alias","test:ssr":"next dev test","test:unit":"npx ts-node test/unit.ts","build:alias":"tsc-alias --outDir dist/esm","test:browser":"vite -c test/vite.config.mts"},"_npmUser":{"name":"canlooks","email":"364021661@qq.com"},"repository":{"url":"git+https://github.com/canlooks/statio.git","type":"git"},"_npmVersion":"11.9.0","description":"Simple react state manager","directories":{},"_nodeVersion":"24.14.0","dependencies":{"tslib":"^2.8.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.2.6","vite":"^8.0.13","react":"^19.2.6","zustand":"^5.0.13","react-dom":"^19.2.6","tsc-alias":"^1.8.17","typescript":"^6.0.3","@types/node":"^25.8.0","@types/react":"^19.2.14","@types/react-dom":"^19.2.3"},"_npmOperationalInternal":{"tmp":"tmp/statio_1.0.2_1779099037591_0.7069533732785875","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@canlooks/statio","version":"1.0.3","keywords":["react","state manager"],"author":{"name":"C.CanLiang","email":"canlooks@gmail.com"},"license":"MIT","_id":"@canlooks/statio@1.0.3","maintainers":[{"name":"canlooks","email":"364021661@qq.com"}],"homepage":"https://github.com/canlooks/statio","bugs":{"url":"https://github.com/canlooks/statio/issues","email":"canlooks@gmail.com"},"dist":{"shasum":"4a8e681299e65923f14344b77d0e754151e3c663","tarball":"https://registry.npmjs.org/@canlooks/statio/-/statio-1.0.3.tgz","fileCount":18,"integrity":"sha512-PPztr8ED92nyRHpe+pdq52QeKo4ruedETJt49RmF/E+CO+tk5zJz8Cw3KLVuS1B4FQjOn20qbt4RbTQhOgC9PQ==","signatures":[{"sig":"MEUCIHg16KuPemfZjQA1gxeyYydc3cmp5en7K3h1ZcboC6c5AiEAkgWRvVyA9gJz5y6R35G5fs89sZWR9TPhawz+txpe1mY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":42192},"main":"dist/cjs/index.js","types":"index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"0c07d822b17ef917d8fdb79a65d70ffc26d80e34","scripts":{"test":"vitest run -c test/vitest.config.ts","build":"tsc -m esnext --outDir dist/esm & tsc -m commonjs --outDir dist/cjs","clean":"npx shx rm -rf dist","rebuild":"npm run clean && npm run build && npm run build:alias","test:ssr":"next dev test","test:watch":"vitest -c test/vitest.config.ts","build:alias":"tsc-alias --outDir dist/esm","test:browser":"vite -c test/vite.config.mts","test:coverage":"vitest run -c test/vitest.config.ts --coverage","test:standalone":"npx ts-node test/standalone.ts"},"_npmUser":{"name":"canlooks","email":"364021661@qq.com"},"repository":{"url":"git+https://github.com/canlooks/statio.git","type":"git"},"_npmVersion":"11.9.0","description":"Simple react state manager","directories":{},"_nodeVersion":"24.14.0","dependencies":{"tslib":"^2.8.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.2.6","vite":"^8.0.13","react":"^19.2.6","vitest":"^4.1.6","zustand":"^5.0.13","react-dom":"^19.2.6","tsc-alias":"^1.8.17","typescript":"^6.0.3","@types/node":"^25.8.0","@types/react":"^19.2.14","@types/react-dom":"^19.2.3"},"_npmOperationalInternal":{"tmp":"tmp/statio_1.0.3_1779180019702_0.21785336871425298","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@canlooks/statio","version":"1.0.4","keywords":["react","state manager"],"author":{"name":"C.CanLiang","email":"canlooks@gmail.com"},"license":"MIT","_id":"@canlooks/statio@1.0.4","maintainers":[{"name":"canlooks","email":"364021661@qq.com"}],"homepage":"https://github.com/canlooks/statio","bugs":{"url":"https://github.com/canlooks/statio/issues","email":"canlooks@gmail.com"},"dist":{"shasum":"0c85ccd06b2dd71452391b8d7f94265ff7e0828b","tarball":"https://registry.npmjs.org/@canlooks/statio/-/statio-1.0.4.tgz","fileCount":18,"integrity":"sha512-ta/zGn8MO2E2BGJTJhpLNntq2WFRfNytcAHTNFKakCzYah85ArGuz7ZSauLPvbj+MPrigG/P6+UwjnOCcCHHtQ==","signatures":[{"sig":"MEYCIQD1WVtOfIl9nfJRLZqQ8d6VqiJqmPWeiBtYqzMqGMf5HAIhAIkrZLeVTy7R8BdUBQfpgl6ZVEBT2VJYXj9wlt7eRGa+","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":43091},"main":"dist/cjs/index.js","types":"index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"f385d25f5168bb7c12f64164349fa482f01747e1","scripts":{"test":"vitest run -c test/vitest.config.ts","build":"tsc -m esnext --outDir dist/esm & tsc -m commonjs --outDir dist/cjs","clean":"npx shx rm -rf dist","rebuild":"npm run clean && npm run build && npm run build:alias","test:ssr":"next dev test","test:node":"npx ts-node test/node.ts","test:watch":"vitest -c test/vitest.config.ts","build:alias":"tsc-alias --outDir dist/esm","test:browser":"vite -c test/vite.config.mts","test:coverage":"vitest run -c test/vitest.config.ts --coverage"},"_npmUser":{"name":"canlooks","email":"364021661@qq.com"},"repository":{"url":"git+https://github.com/canlooks/statio.git","type":"git"},"_npmVersion":"11.13.0","description":"Simple react state manager","directories":{},"_nodeVersion":"24.14.1","dependencies":{"tslib":"^2.8.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.2.6","vite":"^8.0.13","react":"^19.2.6","vitest":"^4.1.6","zustand":"^5.0.13","react-dom":"^19.2.6","tsc-alias":"^1.8.17","typescript":"^6.0.3","@types/node":"^25.8.0","@types/react":"^19.2.14","@types/react-dom":"^19.2.3","@vitest/coverage-v8":"^4.1.6"},"_npmOperationalInternal":{"tmp":"tmp/statio_1.0.4_1780761646450_0.7238837890249739","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"_id":"@canlooks/statio@1.1.0","bugs":{"url":"https://github.com/canlooks/statio/issues","email":"canlooks@gmail.com"},"dist":{"shasum":"637db26e700c7cde51b432adfbcd8a5bd9c9f801","tarball":"https://registry.npmjs.org/@canlooks/statio/-/statio-1.1.0.tgz","fileCount":18,"integrity":"sha512-59idJuqUJ3YBPR0osK2Ui30OkHPrypRtNtKBCXmGqONQrsTl+MNC+Hv7Se4FZJGU1cPaltfvt3Kq/G69i1UPcQ==","signatures":[{"sig":"MEYCIQCNeOyhlQUMlbaEtSmtP5/RCKcWIf/7coV/y4iE9PQ43QIhANK5J5owPdEaix7wLTaU9UdhNC+DWQFkrqXbctzMRrZD","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICWI5mnRrInvlUHpwNq+MFMej90foMCT6Rfn1djmd3KBAiASkBh3dhsITGLn8eODgRcMIdswHR7zviLYXZnTqqcb4w=="}],"unpackedSize":63901},"main":"dist/cjs/index.js","name":"@canlooks/statio","types":"index.d.ts","author":{"name":"C.CanLiang","email":"canlooks@gmail.com"},"module":"dist/esm/index.js","exports":{".":{"types":"./index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"c7d463a8bf9e584029b021e2c2baa26043fdaebe","license":"MIT","scripts":{"test":"vitest run -c test/vitest.config.ts","build":"tsc -m esnext --outDir dist/esm && tsc -m commonjs --outDir dist/cjs","clean":"npx shx rm -rf dist","rebuild":"npm run clean && npm run build && npm run build:alias","test:ssr":"vitest run -c test/vitest.config.ts test/integration/ssr.test.tsx test/integration/hydration.test.tsx","test:node":"vitest run -c test/vitest.config.ts test/unit test/integration/ssr.test.tsx","test:types":"tsc -p test/tsconfig.json","test:watch":"vitest -c test/vitest.config.ts","build:alias":"tsc-alias --outDir dist/esm","test:browser":"vite -c test/vite.config.mts","test:package":"vitest run -c test/package.config.ts","test:coverage":"vitest run -c test/vitest.config.ts --coverage","test:acceptance":"node test/run-acceptance.mjs","test:known-issues":"vitest run -c test/known-issues.config.ts"},"version":"1.1.0","_npmUser":{"name":"canlooks","email":"364021661@qq.com"},"homepage":"https://github.com/canlooks/statio","keywords":["react","state manager"],"repository":{"url":"git+https://github.com/canlooks/statio.git","type":"git"},"_npmVersion":"11.19.0","description":"Simple react state manager","directories":{},"maintainers":[{"name":"canlooks","email":"364021661@qq.com"}],"_nodeVersion":"24.14.1","dependencies":{"tslib":"^2.8.1"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"next":"^16.2.6","vite":"^8.0.13","jsdom":"28.1.0","react":"^19.2.6","vitest":"4.1.5","react-dom":"^19.2.6","tsc-alias":"^1.9.4","typescript":"^6.0.3","@types/node":"^25.8.0","@types/react":"^19.2.14","@types/react-dom":"^19.2.3","@vitest/coverage-v8":"4.1.5"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/statio_1.1.0_1789011749998_0.7144651956735129"}}},"time":{"created":"2026-05-18T07:47:19.766Z","modified":"2026-09-10T03:42:30.249Z","1.0.0":"2026-05-18T07:47:20.013Z","1.0.1":"2026-05-18T08:08:01.289Z","1.0.2":"2026-05-18T10:10:37.722Z","1.0.3":"2026-05-19T08:40:19.831Z","1.0.4":"2026-06-06T16:00:46.595Z","1.1.0":"2026-09-10T03:42:30.112Z"},"bugs":{"url":"https://github.com/canlooks/statio/issues","email":"canlooks@gmail.com"},"author":{"name":"C.CanLiang","email":"canlooks@gmail.com"},"license":"MIT","homepage":"https://github.com/canlooks/statio","keywords":["react","state manager"],"repository":{"url":"git+https://github.com/canlooks/statio.git","type":"git"},"description":"Simple react state manager","maintainers":[{"name":"canlooks","email":"364021661@qq.com"}],"readme":"# @canlooks/statio\r\n\r\nA lightweight, TypeScript-first state management library for React. Built on top of React's `useSyncExternalStore`, Statio provides a minimal yet powerful API with first-class support for **class-based stores**, **computed properties**, **persistence**, and **SSR** — all without boilerplate.\r\n\r\n## Features\r\n\r\n- **Dual store creation** — factory functions *or* ES6 classes, whichever fits your style\r\n- **Automatic method binding** — `this` in your store methods always points to the current state\r\n- **Computed properties** — memoized derived values that only recalculate when dependencies change\r\n- **Selective re-rendering** — components only update when the slice of state they consume changes\r\n- **Built-in persistence** — `localStorage` / `sessionStorage` support with a single wrapper, customizable storage adapter\r\n- **SSR-ready** — separate `serverState` for hydration, works with Next.js\r\n- **Outside-React access** — read, write, and subscribe to state from anywhere\r\n- **Tiny footprint** — `tslib` plus the host application's React peer\n\r\n## Installation\r\n\r\n```bash\r\nnpm i @canlooks/statio\n```\n\nRequires React 18 or 19 in the host application. React is a required peer dependency; package managers that do not install peers automatically require you to install a compatible React version separately.\n\r\n## Quick Start\r\n\r\n### Creating a Store\r\n\r\nStatio supports two patterns for defining a store. Choose the one you prefer — they have identical runtime behavior.\r\n\r\n#### Factory Function\r\n\r\n```ts\r\nimport { createStore } from '@canlooks/statio'\r\n\r\ninterface CounterStore {\r\n  unused: string\r\n  count: number\r\n  increase(): void\r\n}\r\n\r\nconst useCounterStore = createStore<CounterStore>((set) => ({\r\n  unused: 'unused property',\r\n  count: 1,\r\n  increase() {\r\n    set({ count: this.count + 1 })\r\n  },\r\n}))\r\n```\r\n\r\n#### Class\r\n\r\n```ts\r\nimport { createStore, type SetStateMethod } from '@canlooks/statio'\r\n\r\nclass CounterStore {\r\n  unused = 'unused property'\r\n  count = 1\r\n\r\n  constructor(private set: SetStateMethod<CounterStore>) {}\r\n\r\n  increase() {\r\n    this.set({ count: this.count + 1 })\r\n  }\r\n}\r\n\r\nconst useCounterStore = createStore(CounterStore)\r\n```\r\n\r\n### Using the Store in Components\r\n\r\n```tsx\r\nfunction Counter() {\r\n  const { count, increase } = useCounterStore()\r\n\r\n  return (\r\n    <div>\r\n      <h1>Count: {count}</h1>\r\n      <button onClick={increase}>+1</button>\r\n    </div>\r\n  )\r\n}\r\n```\r\n\r\n### Selective Re-rendering with Selectors\r\n\r\nWithout a selector, the component re-renders on *any* state change. Use a selector function to subscribe to a specific slice:\r\n\r\n```tsx\r\nfunction Counter() {\r\n  // Only re-renders when `count` changes.\r\n  // `unused` and `increase` are ignored by the diff.\r\n  const { count, increase } = useCounterStore((state) => ({\r\n    count: state.count,\r\n    increase: state.increase,\r\n  }))\r\n\r\n  return (\r\n    <div>\r\n      <h1>Count: {count}</h1>\r\n      <button onClick={increase}>+1</button>\r\n    </div>\r\n  )\r\n}\r\n```\r\n\r\n**Key-based selector** (syntactic sugar):\r\n\r\n```tsx\r\nfunction Counter() {\r\n  // Equivalent to: (s) => ({ count: s.count, increase: s.increase })\r\n  const { count, increase } = useCounterStore('count', 'increase')\r\n\r\n  return (\r\n    <div>\r\n      <h1>Count: {count}</h1>\r\n      <button onClick={increase}>+1</button>\r\n    </div>\r\n  )\r\n}\r\n```\r\n\r\n> **How it works**: selectors use `shallowEqual` by default for object results. You can override equality checks by passing a custom `isEqual` function as the second argument to the selector form.\n\nChanging keys, a selector closure, or its comparator takes effect on that render. Symbol selections are returned unchanged. Equal selections reuse their previous result reference; shallow comparison does not track deep mutations inside a selected object.\n\r\n---\r\n\r\n## Advanced Features\r\n\r\n### Computed (Derived) Properties\r\n\r\nStatio provides a `compute` API for memoized derived values. A computed property only re-evaluates when its declared dependencies change.\r\n\r\n```ts\r\nimport { createStore, type SetStateMethod, type StoreApi } from '@canlooks/statio'\r\n\r\ninterface ProductStore {\r\n  items: { name: string; price: number }[]\r\n  readonly totalPrice: number\r\n  readonly itemCount: number\r\n  addItem(item: { name: string; price: number }): void\r\n}\r\n\r\nconst useProductStore = createStore<ProductStore>((set, api) => ({\r\n  items: [],\r\n  get totalPrice() {\r\n    return api.compute(() => {\r\n      return this.items.reduce((sum, i) => sum + i.price, 0)\r\n    }, [this.items])\r\n  },\r\n  get itemCount() {\r\n    return api.compute(() => this.items.length, [this.items])\r\n  },\r\n  addItem(item) {\r\n    set({ items: [...this.items, item] })\r\n  },\r\n}))\r\n```\r\n\r\nWith a class store:\r\n\r\n```ts\r\nclass ProductStore {\r\n  items: { name: string; price: number }[] = []\r\n\r\n  constructor(\r\n    private set: SetStateMethod<ProductStore>,\r\n    private api: StoreApi<ProductStore>,\r\n  ) {}\r\n\r\n  get totalPrice() {\r\n    return this.api.compute(() => {\r\n      return this.items.reduce((sum, i) => sum + i.price, 0)\r\n    }, [this.items])\r\n  }\r\n\r\n  get itemCount() {\r\n    return this.api.compute(() => this.items.length, [this.items])\r\n  }\r\n\r\n  addItem(item: { name: string; price: number }) {\r\n    this.set({ items: [...this.items, item] })\r\n  }\r\n}\r\n```\r\n\r\n**Important**: `compute()` only works inside a getter property. Calling it elsewhere will throw an error.\n\nString and Symbol getters have separate, lazy caches. Falsy results are cached. A throwing calculation propagates its error and can be retried with the same dependencies; it does not replace the last successful cache.\n\r\n### Persistence (`storage`)\r\n\r\nWrap any store factory with `storage()` to automatically persist state to `localStorage` or `sessionStorage`:\r\n\r\n```ts\r\nimport { createStore, storage } from '@canlooks/statio'\r\n\r\nconst useSettingsStore = createStore(\r\n  storage(\r\n    (set) => ({\r\n      theme: 'light' as 'light' | 'dark',\r\n      fontSize: 14,\r\n      setTheme(theme: 'light' | 'dark') {\r\n        set({ theme })\r\n      },\r\n    }),\r\n    { name: 'app-settings' },\r\n  ),\r\n)\r\n```\r\n\r\n**Options:**\r\n\r\n| Option | Type | Default | Description |\r\n|--------|------|---------|-------------|\r\n| `name` | `string` | *required* | Storage key |\r\n| `type` | `'localStorage' \\| 'sessionStorage'` | `'localStorage'` | Storage backend |\r\n| `selector` | `(state: S) => unknown` | excludes methods, Api instances and getters | Custom serialization selector |\n| `adapter` | `{ getItem, setItem }` | `window[type]` | Custom storage adapter (e.g., AsyncStorage for React Native) |\r\n\r\n**How it works:**\r\n\r\n- The default selector excludes methods, Api instances and getters without evaluating getters.\n- Factory `set`, `api.setState` and `useStore.setState` share one batch. Same-tick updates apply synchronously and produce one write with the latest state.\n- A synchronous adapter restores data before `createStore` returns. Initialization never writes it back.\n- PromiseLike reads restore data asynchronously with a shallow merge and notify subscribers and React. Methods and untouched defaults remain available.\n- If any active update occurs while an asynchronous read is pending, the entire late cache is discarded, including fields that the update did not touch. The current state takes priority.\n- Promise writes are serialized per store: the next write starts after the previous one settles. Rejected reads keep the current state; rejected writes keep the applied memory update and allow later writes to continue. Each rejection is reported once with the `[@canlooks/statio]` prefix, without retries.\n- Invalid JSON and empty patches are ignored. The top-level `__proto__` field is discarded before restoring data.\n- Restoration skips getter-only and non-writable fields, so a custom selector may persist a computed value without assigning it back over the getter. Writable data, legal setters and additional fields on extensible states retain shallow assignment behavior. A cache containing only skipped fields does not notify or write back.\n\nSynchronous adapter and serialization errors keep their existing propagation behavior. Exceptions while applying asynchronous restored data, including listener errors, are reported once as `Storage restore failed`; they are not treated as invalid JSON.\n\nDescriptor checks do not evaluate getters and do not provide transactional rollback for user setters or Proxies that throw while applying otherwise writable fields.\n\r\n**Custom adapter example:**\r\n\r\n```ts\r\nstorage(MyStore, {\r\n  name: 'my-store',\r\n  adapter: {\r\n    getItem(key) { return AsyncStorage.getItem(key) },\r\n    setItem(key, value) { return AsyncStorage.setItem(key, value) },\n  },\r\n})\r\n```\r\n\r\n### Accessing State Outside React\r\n\r\nThe `useStore` hook exposes additional methods for imperative use:\r\n\r\n```ts\r\n// Read current state without subscribing\r\nconst currentState = useCounterStore.getState()\r\n\r\n// Update state from outside a component\r\nuseCounterStore.setState({ count: 99 })\r\n\r\n// Subscribe to changes (returns unsubscribe function)\r\nconst unsub = useCounterStore.subscribe((state) => {\r\n  console.log('State changed:', state)\r\n})\r\n\r\n// Subscribe with a selector\r\nconst unsub = useCounterStore.subscribe(\r\n  (state) => state.count,\r\n  (count, prevCount) => {\r\n    console.log(`Count: ${prevCount} → ${count}`)\r\n  },\r\n)\r\n\r\n// Stop listening\r\nuseCounterStore.unsubscribe(listener)\n```\n\nA selector subscription reads its initial value at registration without calling the listener. Its first change receives `(current, valueAtSubscription)`. With `immediate: true`, the existing immediate comparison rules apply and the initial previous value is `undefined`. Selector errors during registration propagate and leave no subscription; errors during updates still propagate. Cleanup functions are idempotent, including after unsubscribe and re-registration.\n\nSubscription comparators must accept an undefined previous value, for example `isEqual: (a, b) => b !== undefined && a.count === b.count`. Hook comparators still receive two defined selections when the selection type itself excludes undefined. The subscription baseline is committed before its listener runs, so a synchronous update from that listener sees the latest selection. Listener errors still propagate and stop subsequent delivery.\n\nWhole-state external subscriptions notify synchronously for every update, including identical values and empty patches. React renders and persistence writes can still batch those updates.\n\r\n### Server-Side Rendering (SSR)\r\n\r\nThe `storage()` middleware captures the initial state using its prototype and property descriptors, without evaluating getters. Server rendering and initial hydration apply the same keys or selector to that snapshot; subsequent client rendering uses restored data. Server and client computed getters have independent caches, so a default count of 1 and a cached count of 9 render a doubled value of 2 on the server and 18 after hydration.\n\nThe server snapshot is shallow: nested objects are shared, and deep mutation tracking is not provided. Client overwrites rebuild client bindings and computed caches while preserving the server defaults. Create request-scoped stores for request-specific server data.\n\n`storage()` preserves a `serverState` explicitly set by your factory. Use this for classes whose server getters access ECMAScript `#private` fields or methods: descriptor copies cannot reproduce private slots. Provide a separate, un-restored instance initialized with the same defaults:\n\n```ts\nclass PrivateCounter {\n  count = 1\n  #api: StoreApi<PrivateCounter>\n\n  constructor(_set: SetStateMethod<PrivateCounter>, api: StoreApi<PrivateCounter>) {\n    this.#api = api\n  }\n\n  get doubled(): number {\n    return this.#api.compute(() => this.count * 2, [this.count])\n  }\n}\n\nconst usePrivateCounter = createStore(storage<PrivateCounter>((set, api) => {\n  const client = new PrivateCounter(set, api)\n  api.serverState = new PrivateCounter(set, api)\n  return client\n}, { name: 'private-counter' }))\n```\n\nImport `SetStateMethod` and `StoreApi` as types from `@canlooks/statio`. The explicit path constructs two instances; the factory controls their initialization effects. Never assign the client instance itself as its server snapshot. Getters remain lazy and use independent server/client caches. Factories without an explicit snapshot still run once. Passing a private class directly to `storage(PrivateCounter, options)` does not automatically make its private getters usable on the copied server snapshot; use this explicit path or a factory closure/ordinary property instead.\n\r\nWhen using Next.js App Router with a persisted store:\r\n\r\n```tsx\r\n// stores/counter.ts\r\nimport { createStore, storage } from '@canlooks/statio'\r\n\r\nclass CounterStore {\r\n  count = 0\r\n  // ...\r\n}\r\n\r\nexport const useCounterStore = createStore(\r\n  storage(CounterStore, { name: 'counter' }),\r\n)\r\n```\r\n\r\n```tsx\r\n// app/page.tsx\r\n'use client'\r\n\r\nimport { useCounterStore } from '@/stores/counter'\r\n\r\nexport default function Page() {\r\n  const { count } = useCounterStore('count')\r\n  return <div>{count}</div>\r\n}\r\n```\r\n\r\n### Batching Updates\r\n\r\nMultiple synchronous `set()` calls are automatically batched by React 18's automatic batching. For external usage, Statio provides `createBatchAction`:\r\n\r\n```ts\r\nimport { createBatchAction } from '@canlooks/statio'\r\n\r\nconst batchedSet = createBatchAction(\r\n  useCounterStore.setState,\r\n  () => console.log('All updates applied'),\r\n)\r\n\r\n// These two calls trigger the effect only once\r\nbatchedSet({ count: 1 })\r\nbatchedSet({ count: 2 })\r\n```\r\n\r\n---\r\n\r\n## API Reference\r\n\r\n### `createStore(factory)`\r\n\r\n```ts\r\nfunction createStore<S extends object>(\r\n  factory: StoreFactory<S> | StoreClass<S>\r\n): UseStoreHook<S>\r\n```\r\n\r\nCreates a store and returns a `useStore` hook. The factory receives two arguments:\r\n\r\n| Parameter | Type | Description |\r\n|-----------|------|-------------|\r\n| `set` | `SetStateMethod<S>` | Update state (partial or updater function) |\r\n| `api` | `StoreApi<S>` | Store API (state, getState, compute, etc.) |\r\n\r\n**Returned hook signatures:**\r\n\r\n```ts\r\n// Subscribe to entire state (re-renders on any change)\r\nuseStore(): S\r\n\r\n// Subscribe to specific keys (shorthand selector)\r\nuseStore(...keys: (keyof S)[]): Pick<S, typeof keys[number]>\r\n\r\n// Subscribe with a custom selector\r\nuseStore<T>(selector: (state: S) => T, isEqual?: IsEqual<T>): T\r\n\r\n// Imperative methods attached to the hook\r\nuseStore.getState(): S\r\nuseStore.setState: SetStateMethod<S>\r\nuseStore.subscribe(...): () => void\r\nuseStore.unsubscribe(listener: Function): void\r\n```\r\n\r\n### `SetStateMethod<S>`\r\n\r\n```ts\r\ntype SetStateMethod<S> = (\r\n  state: Partial<S> | ((state: S) => Partial<S>),\r\n  overwrite?: boolean,\r\n) => void\r\n```\r\n\r\n- **Partial update** (default): merges the provided partial into existing state via `Object.assign`\r\n- **Updater function**: receives current state, returns a partial to merge\r\n- **Overwrite mode** (`overwrite = true`): replaces the entire state object and re-binds methods/computed properties. Use sparingly — typically only for hydration\n\nThe Hook's `setState` also provides a complete-state overload for inline replacements, such as `store.setState({count: 10, read() { return this.count }}, true)`. Partial and updater calls keep their existing signatures. Reusing methods or getter descriptors previously wrapped by that store rebinds their original definitions to the new root. External bound functions, arrow functions and business closures keep their original context; spreading an object evaluates its getters and does not preserve their descriptors.\n\r\n### `StoreApi<S>`\r\n\r\nPassed as the second argument to store factories/constructors:\r\n\r\n```ts\r\ninterface StoreApi<S> {\n  state: S             // Current state object\r\n  serverState?: S      // Server-side snapshot (for SSR, set by storage middleware)\r\n  setState: SetStateMethod<S>\r\n  getState(): S\r\n  compute: Compute      // Memoized derived value helper\r\n  computable: Computable<S>\r\n}\n```\n\n`StoreApi<S>` is a type. The runtime constructor is exported as `Api`: `new Api(factory, onChange)` exposes this interface. `Computable` and `createCompute` are also available from the package root.\n\r\n### `storage(factory, options)`\r\n\r\n```ts\r\nfunction storage<S extends object>(\r\n  factory: StoreFactory<S> | StoreClass<S>,\r\n  options: StorageOptions<S>,\r\n): StoreFactory<S>\r\n```\r\n\r\nWraps a store factory with persistence. Returns a new factory to pass to `createStore`.\r\n\r\n**`StorageOptions<S>`:**\r\n\r\n```ts\r\ntype StorageOptions<S> = {\r\n  name: string\r\n  type?: 'localStorage' | 'sessionStorage'        // default: 'localStorage'\r\n  selector?: (state: S) => unknown               // custom serialization\n  adapter?: {\n    getItem(key: string): string | null | PromiseLike<string | null>\n    setItem(key: string, value: string): void | PromiseLike<void>\n  }\r\n}\r\n```\r\n\r\n### `compute` / `Computable`\r\n\r\n```ts\r\ntype Compute = <T>(factory: () => T, deps: any[]) => T\nfunction createCompute(): Compute\n\nclass Computable<S> {\n  constructor(state: S)\n  createGetter<T>(key: PropertyKey, get: () => T): () => T\n  get<T>(factory: () => T, deps: any[]): T\n}\n```\r\n\r\nThe `api.compute()` method memoizes a computation based on a dependency array. It uses `shallowEqual` to compare deps — if they haven't changed since the last call, the cached result is returned immediately.\r\n\r\nMust be called inside a getter property. Each getter gets its own independent memoization cache.\r\n\r\n### Utility Functions\r\n\r\n#### `shallowEqual(a, b)`\r\n\r\n```ts\r\nfunction shallowEqual(a: any, b: any): boolean\r\n```\r\n\r\nCompares own enumerable string and Symbol keys and their values using `===`. Non-enumerable and inherited fields are ignored; `NaN` differs from itself and `0` equals `-0`. Used internally for selector comparison and compute dependency diffing.\n\r\n#### `nextTick(callback?, ...args)`\r\n\r\n```ts\r\nfunction nextTick<T>(callback?: (...args: T[]) => void, ...args: T[]): AbortablePromise<T>\r\n```\r\n\r\nSchedules a callback for the next microtask (using `queueMicrotask`). Returns an abortable promise — call `.abort()` to cancel. An aborted pending promise remains unsettled.\n\r\n#### `createBatchAction(action, effect)`\r\n\r\n```ts\r\nfunction createBatchAction<T extends (...a: any[]) => any>(\r\n  action: T,\r\n  effect: () => any,\r\n): T\r\n```\r\n\r\nWraps an action function so that multiple synchronous invocations only trigger the `effect` once (on the next microtask). Preserves `this`, arguments and the action's return value, including the original Promise reference. A thrown action still schedules the effect and propagates the error. Used internally by `storage()` to debounce writes.\n\r\n#### `isClass(fn)`\r\n\r\n```ts\r\nfunction isClass(fn: Function): fn is StoreClass\r\n```\r\n\r\nType guard that checks whether a function is an ES6 class constructor.\r\n\r\n#### `getAllPropertyDescriptors(o)`\r\n\r\n```ts\r\nfunction getAllPropertyDescriptors(o: any): { [p: PropertyKey]: PropertyDescriptor }\r\n```\r\n\r\nReturns all property descriptors of an object, including those inherited from the prototype chain (stops at `null`, `Object.prototype`, `Array.prototype`, and `Function.prototype`). Null-prototype stores and Symbol methods are supported.\n\r\n---\r\n\r\n## TypeScript\r\n\r\nStatio is written in TypeScript and provides first-class type inference. Store state is fully typed:\r\n\r\n```ts\r\ninterface TodoStore {\r\n  todos: { id: number; text: string; done: boolean }[]\r\n  readonly activeCount: number\r\n  addTodo(text: string): void\r\n  toggleTodo(id: number): void\r\n}\r\n\r\n// Full type safety — IDE autocompletion on all state properties and methods\r\nconst useTodoStore = createStore<TodoStore>((set, api) => ({\r\n  todos: [],\r\n  get activeCount() {\r\n    return api.compute(() => this.todos.filter(t => !t.done).length, [this.todos])\r\n  },\r\n  addTodo(text) {\r\n    set({ todos: [...this.todos, { id: Date.now(), text, done: false }] })\r\n  },\r\n  toggleTodo(id) {\r\n    set({\r\n      todos: this.todos.map(t =>\r\n        t.id === id ? { ...t, done: !t.done } : t\r\n      ),\r\n    })\r\n  },\r\n}))\r\n```\r\n\r\nSelectors are also fully typed — the return type is inferred from the selector function.\n\nFactories with injected `set`/`api`, including factories wrapped by `storage`, infer the returned state fields, Hook selections and external `setState` patches. Inference also preserves readonly getters and Symbol fields. Inside a self-referencing factory with no explicit state type, the injected `set`/`api` can still have broad types. Use `createStore<State>` / `storage<State>` or explicitly annotate the factory parameters when you need checks inside the factory. Incompatible explicit parameter types are rejected.\n\r\n---\r\n\r\n## License\r\n\r\nMIT\r\n","readmeFilename":"README.md"}