{"id":13525833,"url":"https://github.com/Marcisbee/exome","last_synced_at":"2025-04-01T05:32:32.458Z","repository":{"id":46179307,"uuid":"353644602","full_name":"Marcisbee/exome","owner":"Marcisbee","description":"🔅 State manager for deeply nested states","archived":false,"fork":false,"pushed_at":"2025-02-27T07:04:13.000Z","size":3495,"stargazers_count":270,"open_issues_count":2,"forks_count":10,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-24T00:11:18.995Z","etag":null,"topics":["exome","javascript","js","react","state","state-management","state-manager","state-tree","typescript"],"latest_commit_sha":null,"homepage":"https://exome.js.org","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Marcisbee.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-04-01T09:24:55.000Z","updated_at":"2025-03-20T20:30:01.000Z","dependencies_parsed_at":"2024-01-29T10:09:30.413Z","dependency_job_id":"99954956-f0c7-41c7-88b8-3a9dda4592ba","html_url":"https://github.com/Marcisbee/exome","commit_stats":{"total_commits":192,"total_committers":3,"mean_commits":64.0,"dds":0.01041666666666663,"last_synced_commit":"917c73a772b3e9af30c7cdc2f93df1c36dccbe3a"},"previous_names":[],"tags_count":78,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Marcisbee%2Fexome","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Marcisbee%2Fexome/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Marcisbee%2Fexome/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Marcisbee%2Fexome/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Marcisbee","download_url":"https://codeload.github.com/Marcisbee/exome/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246531975,"owners_count":20792736,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["exome","javascript","js","react","state","state-management","state-manager","state-tree","typescript"],"created_at":"2024-08-01T06:01:22.665Z","updated_at":"2025-04-01T05:32:32.435Z","avatar_url":"https://github.com/Marcisbee.png","language":"TypeScript","readme":"\u003ca href=\"../../\"\u003e\u003cimg src=\"logo/logo-title-dark.png\" width=\"220\" /\u003e\u003c/a\u003e\n\n\u003ca href=\"https://github.com/Marcisbee/exome/actions\"\u003e\n  \u003cimg alt=\"CI\" src=\"https://img.shields.io/github/actions/workflow/status/Marcisbee/exome/main.yml?branch=main\u0026style=flat-square\" /\u003e\n\u003c/a\u003e\n\u003ca href=\"https://www.npmjs.com/package/exome\"\u003e\n  \u003cimg alt=\"npm\" src=\"https://img.shields.io/npm/v/exome.svg?style=flat-square\" /\u003e\n\u003c/a\u003e\n\u003ca href=\"https://jsr.io/@exome/exome\"\u003e\n  \u003cimg alt=\"jsr\" src=\"https://jsr.io/badges/@exome/exome?style=flat-square\" /\u003e\n\u003c/a\u003e\n\u003ca href=\"https://bundlephobia.com/result?p=exome\"\u003e\n  \u003cimg alt=\"package size\" src=\"https://deno.bundlejs.com/?q=exome\u0026badge=\u0026badge-style=flat-square\" /\u003e\n\u003c/a\u003e\n\u003ca href=\"https://discord.gg/a62gfaDW2e\"\u003e\n  \u003cimg alt=\"discord\" src=\"https://dcbadge.vercel.app/api/server/a62gfaDW2e?style=flat-square\" /\u003e\n\u003c/a\u003e\n\nState manager for deeply nested states. Includes integration for [React](#react), [Preact](#preact), [Vue](#vue), [Svelte](#svelte), [Solid](#solid), [Lit](#lit), [Rxjs](#rxjs), [Angular](#angular) \u0026 [No framework](#no-framework). Can be easily used in microfrontends architecture.\n\n# Features\n\n- 📦 **Small**: Just **1 KB** minizipped\n- 🚀 **Fast**: Uses **no diffing** of state changes see [**benchmarks**](https://marcisbee.com/js-store-benchmark?focus=exome)\n- 😍 **Simple**: Uses classes as state, methods as actions\n- 🛡 **Typed**: Written in strict TypeScript\n- 🔭 **Devtools**: Redux devtools integration\n- 💨 **Zero dependencies**\n\n```ts\n// store/counter.ts\nimport { Exome } from \"exome\"\n\nexport class Counter extends Exome {\n  public count = 0\n\n  public increment() {\n    this.count += 1\n  }\n}\n\nexport const counter = new Counter()\n```\n\n```tsx\n// components/counter.tsx\nimport { useStore } from \"exome/react\"\nimport { counter } from \"../stores/counter.ts\"\n\nexport default function App() {\n  const { count, increment } = useStore(counter)\n\n  return (\n    \u003ch1 onClick={increment}\u003e{count}\u003c/h1\u003e\n  )\n}\n```\n\n[__Simple Demo__](https://dune.land/dune/468e79c1-e31b-4035-bc19-b03dfa363060)\n\n# Table of contents\n\n- [Core concepts](#core-concepts)\n- [Usage](#usage)\n- Integration\n  - [React](#react)\n  - [Preact](#preact)\n  - [Vue](#vue)\n  - [Svelte](#svelte)\n  - [Solid](#solid)\n  - [Lit](#lit)\n  - [Rxjs](#rxjs)\n  - [Angular](#angular)\n  - [No framework](#no-framework)\n- [Redux devtools](#redux-devtools)\n- [API](#api)\n- [FAQ](#faq)\n- [**Benchmarks**](https://marcisbee.com/js-store-benchmark?focus=exome)\n- [Motivation](#motivation)\n\n# Installation\nTo install the stable version:\n```bash\nnpm install --save exome\n```\nThis assumes you are using [npm](https://www.npmjs.com/package/exome) as your package manager.\n\n# Core concepts\nAny piece of state you have, must use a class that extends `Exome`.\n\n`Stores`\n\nStore can be a single class or multiple ones. I'd suggest keeping stores small, in terms of property sizes.\n\n`State values`\n\nRemember that this is quite a regular class (with some behind the scenes logic). So you can write you data inside properties however you'd like. Properties can be public, private, object, arrays, getters, setters, static etc.\n\n`Actions`\n\nEvery method in class is considered as an action. They are only for changing state. Whenever any method is called in Exome it triggers update to middleware and updates view components. Actions can be regular methods or even async ones.\n\nIf you want to get something from state via method, use getters.\n\n# Usage\nLibrary can be used without typescript, but I mostly recommend using it with typescript as it will guide you through what can and cannot be done as there are no checks without it and can lead to quite nasty bugs.\n\nTo create a typed store just create new class with a name of your choosing by extending `Exome` class exported from `exome` library.\n\n```ts\nimport { Exome } from \"exome\"\n\n// We'll have a store called \"CounterStore\"\nclass CounterStore extends Exome {\n  // Lets set up one property \"count\" with default value \"0\"\n  public count = 0\n\n  // Now lets create action that will update \"count\" value\n  public increment() {\n    this.count += 1\n  }\n}\n```\n[__Open in dune.land__](https://dune.land/dune/468e79c1-e31b-4035-bc19-b03dfa363060)\n\nThat is the basic structure of simple store. It can have as many properties as you'd like. There are no restrictions.\n\nNow we should create an instance of `CounterStore` to use it.\n\n```ts\nconst counter = new CounterStore()\n```\n\nNice! Now we can start using `counter` state.\n\n# Integration\n## React\nUse `useStore()` from `exome/react` to get store value and re-render component on store change.\n\n```tsx\nimport { useStore } from \"exome/react\"\nimport { counter } from \"../stores/counter.ts\"\n\nexport function Example() {\n  const { count, increment } = useStore(counter)\n  return \u003cbutton onClick={increment}\u003e{count}\u003c/button\u003e\n}\n```\n\n## Preact\nUse `useStore()` from `exome/preact` to get store value and re-render component on store change.\n\n```tsx\nimport { useStore } from \"exome/preact\"\nimport { counter } from \"../stores/counter.ts\"\n\nexport function Example() {\n  const { count, increment } = useStore(counter)\n  return \u003cbutton onClick={increment}\u003e{count}\u003c/button\u003e\n}\n```\n\n## Vue\nUse `useStore()` from `exome/vue` to get store value and re-render component on store change.\n\n```html\n\u003cscript lang=\"ts\" setup\u003e\n  import { useStore } from \"exome/vue\";\n  import { counter } from \"./store/counter.ts\";\n\n  const { count, increment } = useStore(counter);\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cbutton @click=\"increment()\"\u003e{{ count }}\u003c/button\u003e\n\u003c/template\u003e\n```\n\n## Svelte\nUse `useStore()` from `exome/svelte` to get store value and re-render component on store change.\n\n```html\n\u003cscript\u003e\n  import { useStore } from \"exome/svelte\"\n  import { counter } from \"./store/counter.js\"\n\n  const { increment } = counter\n  const count = useStore(counter, s =\u003e s.count)\n\u003c/script\u003e\n\n\u003cmain\u003e\n  \u003cbutton on:click={increment}\u003e{$count}\u003c/button\u003e\n\u003c/main\u003e\n```\n\n## Solid\nUse `useStore()` from `exome/solid` to get store value and update signal selector on store change.\n\n```tsx\nimport { useStore } from \"exome/solid\"\nimport { counter } from \"../stores/counter.ts\"\n\nexport function Example() {\n  const count = useStore(counter, s =\u003e s.count)\n  return \u003cbutton onClick={counter.increment}\u003e{count}\u003c/button\u003e\n}\n```\n\n## Lit\nUse `StoreController` from `exome/lit` to get store value and re-render component on store change.\n\n```ts\nimport { StoreController } from \"exome/lit\"\nimport { counter } from \"./store/counter.js\"\n\n@customElement(\"counter\")\nclass extends LitElement {\n  private counter = new StoreController(this, counter);\n\n  render() {\n    const { count, increment } = this.counter.store;\n\n    return html`\n      \u003ch1 @click=${increment}\u003e${count}\u003c/h1\u003e\n    `;\n  }\n}\n```\n\n## Rxjs\nUse `observableFromExome` from `exome/rxjs` to get store value as Observable and trigger it when it changes.\n\n```ts\nimport { observableFromExome } from \"exome/rxjs\"\nimport { counter } from \"./store/counter.js\"\n\nobservableFromExome(countStore)\n  .pipe(\n    map(({ count }) =\u003e count),\n    distinctUntilChanged()\n  )\n  .subscribe((value) =\u003e {\n    console.log(\"Count changed to\", value);\n  });\n\nsetInterval(counter.increment, 1000);\n```\n\n## Angular\n### signals (\u003e=16)\nUse `useStore` from `exome/angular` to get store value and update signal selector on store change.\n\n```ts\nimport { useStore } from \"exome/angular\"\nimport { counter } from \"./store/counter.ts\"\n\n@Component({\n  selector: 'my-app',\n  template: `\n    \u003ch1 (click)=\"increment()\"\u003e\n      {{count}}\n    \u003c/h1\u003e\n  `,\n})\nexport class App {\n  public count = useStore(counter, (s) =\u003e s.count);\n  public increment() {\n    counter.increment();\n  }\n}\n```\n\n### observables (\u003c=15)\nAngular support is handled via rxjs async pipes!\n\nUse `observableFromExome` from `exome/rxjs` to get store value as Observable and trigger it when it changes.\n\n```ts\nimport { observableFromExome } from \"exome/rxjs\"\nimport { counter } from \"./store/counter.ts\"\n\n@Component({\n  selector: 'my-app',\n  template: `\n    \u003ch1 *ngIf=\"(counter$ | async) as counter\" (click)=\"counter.increment()\"\u003e\n      {{counter.count}}\n    \u003c/h1\u003e\n  `,\n})\nexport class App {\n  public counter$ = observableFromExome(counter)\n}\n```\n\n## No framework\nUse `subscribe` from `exome` to get store value in subscription callback event when it changes.\n\n```ts\nimport { subscribe } from \"exome\"\nimport { counter } from \"./store/counter.js\"\n\nconst unsubscribe = subscribe(counter, ({ count }) =\u003e {\n  console.log(\"Count changed to\", count)\n})\n\nsetInterval(counter.increment, 1000)\nsetTimeout(unsubscribe, 5000)\n```\n\n# Redux devtools\n\nYou can use redux devtools extension to explore Exome store chunk by chunk.\n\nJust add `exomeReduxDevtools` middleware via `addMiddleware` function exported by library before you start defining store.\n\n```ts\nimport { addMiddleware } from 'exome'\nimport { exomeReduxDevtools } from 'exome/devtools'\n\naddMiddleware(\n  exomeReduxDevtools({\n    name: 'Exome Playground'\n  })\n)\n```\n\nIt all will look something like this:\n\n![Exome using Redux Devtools](https://user-images.githubusercontent.com/16621507/115083737-871c3d00-9f10-11eb-94e7-21353d093a7e.png)\n\n# API\n### `Exome`\nA class with underlying logic that handles state changes. Every store must be extended from this class.\n\n```ts\nabstract class Exome {}\n```\n\n### `useStore`\nIs function exported from \"exome/react\".\n\n```ts\nfunction useStore\u003cT extends Exome\u003e(store: T): Readonly\u003cT\u003e\n```\n\n__Arguments__\n1. `store` _([Exome](#exome))_: State to watch changes from. Without Exome being passed in this function, react component will not be updated when particular Exome updates.\n\n__Returns__\n\n- [_Exome_](#exome): Same store is returned.\n\n__Example__\n\n```tsx\nimport { useStore } from \"exome/react\"\n\nconst counter = new Counter()\n\nfunction App() {\n  const { count, increment } = useStore(counter)\n\n  return \u003cbutton onClick={increment}\u003e{count}\u003c/button\u003e\n}\n```\n[__Open in dune.land__](https://dune.land/dune/468e79c1-e31b-4035-bc19-b03dfa363060)\n\n### `onAction`\nFunction that calls callback whenever specific action on Exome is called.\n\n```ts\nfunction onAction(store: typeof Exome): Unsubscribe\n```\n\n__Arguments__\n1. `store` _([Exome](#exome) constructor)_: Store that has desired action to listen to.\n2. `action` _(string)_: method (action) name on store instance.\n3. `callback` _(Function)_: Callback that will be triggered before or after action.\u003cbr\u003e\n   __Arguments__\n   - `instance` _([Exome](#exome))_: Instance where action is taking place.\n   - `action` _(String)_: Action name.\n   - `payload` _(any[])_: Array of arguments passed in action.\u003cbr\u003e\n4. `type` _(\"before\" | \"after\")_: when to run callback - before or after action, default is `\"after\"`.\n\n__Returns__\n\n- _Function_: Unsubscribes this action listener\n\n__Example__\n\n```tsx\nimport { onAction } from \"exome\"\n\nconst unsubscribe = onAction(\n  Person,\n  'rename',\n  (instance, action, payload) =\u003e {\n    console.log(`Person ${instance} was renamed to ${payload[0]}`);\n\n    // Unsubscribe is no longer needed\n    unsubscribe();\n  },\n  'before'\n)\n```\n\n### `saveState`\nFunction that saves snapshot of current state for any Exome and returns string.\n\n```ts\nfunction saveState(store: Exome): string\n```\n\n__Arguments__\n1. `store` _([Exome](#exome))_: State to save state from (will save full state tree with nested Exomes).\n\n__Returns__\n\n- _String_: Stringified Exome instance\n\n__Example__\n\n```tsx\nimport { saveState } from \"exome/state\"\n\nconst saved = saveState(counter)\n```\n\n### `loadState`\nFunction that loads saved state in any Exome instance.\n\n```ts\nfunction loadState(\n  store: Exome,\n  state: string\n): Record\u003cstring, any\u003e\n```\n\n__Arguments__\n1. `store` _([Exome](#exome))_: Store to load saved state to.\n2. `state` _(String)_: Saved state string from `saveState` output.\n\n__Returns__\n\n- _Object_: Data that is loaded into state, but without Exome instance (if for any reason you have to have this data).\n\n__Example__\n\n```ts\nimport { loadState, registerLoadable } from \"exome/state\"\n\nregisterLoadable({\n  Counter\n})\n\nconst newCounter = new Counter()\n\nconst loaded = loadState(newCounter, saved)\nloaded.count // e.g. = 15\nloaded.increment // undefined\n\nnewCounter.count // new counter instance has all of the state applied so also = 15\nnewCounter.increment // [Function]\n```\n\n### `registerLoadable`\nFunction that registers Exomes that can be loaded from saved state via [`loadState`](#loadState).\n\n```ts\nfunction registerLoadable(\n  config: Record\u003cstring, typeof Exome\u003e,\n): void\n```\n\n__Arguments__\n1. `config` _(Object)_: Saved state string from `saveState` output.\n   - key _(String)_: Name of the Exome state class (e.g. `\"Counter\"`).\n   - value _([Exome](#exome) constructor)_: Class of named Exome (e.g. `Counter`).\n\n__Returns__\n\n- _void_\n\n__Example__\n\n```ts\nimport { loadState, registerLoadable } from \"exome/state\"\n\nregisterLoadable({\n  Counter,\n  SampleStore\n})\n```\n\n### `addMiddleware`\nFunction that adds middleware to Exome. It takes in callback that will be called every time before an action is called.\n\nReact hook integration is actually a middleware.\n\n```ts\ntype Middleware = (instance: Exome, action: string, payload: any[]) =\u003e (void | Function)\n\nfunction addMiddleware(fn: Middleware): void\n```\n\n__Arguments__\n1. `callback` _(Function)_: Callback that will be triggered `BEFORE` action is started.\u003cbr\u003e\n   __Arguments__\n   - `instance` _([Exome](#exome))_: Instance where action is taking place.\n   - `action` _(String)_: Action name.\n   - `payload` _(any[])_: Array of arguments passed in action.\u003cbr\u003e\n\n   __Returns__\n   - _(void | Function)_: Callback can return function that will be called `AFTER` action is completed.\n\n__Returns__\n\n- _void_: Nothingness...\n\n__Example__\n\n```ts\nimport { Exome, addMiddleware } from \"exome\"\n\naddMiddleware((instance, name, payload) =\u003e {\n  if (!(instance instanceof Timer)) {\n    return;\n  }\n\n  console.log(`before action \"${name}\"`, instance.time);\n\n  return () =\u003e {\n    console.log(`after action \"${name}\"`, instance.time);\n  };\n});\n\nclass Timer extends Exome {\n  public time = 0;\n\n  public increment() {\n    this.time += 1;\n  }\n}\n\nconst timer = new Timer()\n\nsetInterval(timer.increment, 1000)\n\n// \u003e before action \"increment\", 0\n// \u003e after action \"increment\", 1\n//   ... after 1s\n// \u003e before action \"increment\", 1\n// \u003e after action \"increment\", 2\n//   ...\n```\n[__Open in Codesandbox__](https://codesandbox.io/s/exome-middleware-ro6of?file=/src/App.tsx)\n\n# FAQ\n### Q: Can I use Exome inside Exome?\nYES! It was designed for that exact purpose.\nExome can have deeply nested Exomes inside itself. And whenever new Exome is used in child component, it has to be wrapped in `useStore` hook and that's the only rule.\n\nFor example:\n```tsx\nclass Todo extends Exome {\n  constructor(public message: string, public completed = false) {\n    super();\n  }\n\n  public toggle() {\n    this.completed = !this.completed;\n  }\n}\n\nclass Store extends Exome {\n  constructor(public list: Todo[]) {\n    super();\n  }\n}\n\nconst store = new Store([\n  new Todo(\"Code a new state library\", true),\n  new Todo(\"Write documentation\")\n]);\n\nfunction TodoView({ todo }: { todo: Todo }) {\n  const { message, completed, toggle } = useStore(todo);\n\n  return (\n    \u003cli\u003e\n      \u003cstrong\n        style={{\n          textDecoration: completed ? \"line-through\" : \"initial\"\n        }}\n      \u003e\n        {message}\n      \u003c/strong\u003e\n      \u0026nbsp;\n      \u003cbutton onClick={toggle}\u003etoggle\u003c/button\u003e\n    \u003c/li\u003e\n  );\n}\n\nfunction App() {\n  const { list } = useStore(store);\n\n  return (\n    \u003cul\u003e\n      {list.map((todo) =\u003e (\n        \u003cTodoView key={getExomeId(todo)} todo={todo} /\u003e\n      ))}\n    \u003c/ul\u003e\n  );\n}\n```\n[__Open in dune.land__](https://dune.land/dune/e557f934-e0b6-4ef7-96c0-cd9ff12c7c0b)\n\n### Q: Can deep state structure be saved to string and then loaded back as an instance?\nYES! This was also one of key requirements for this. We can save full state from any Exome with [`saveState`](#saveState), save it to file or database and the load that string up onto Exome instance with [`loadState`](#loadState).\n\nFor example:\n```tsx\nconst savedState = saveState(store)\n\nconst newStore = new Store()\n\nloadState(newStore, savedState)\n```\n\n### Q: Can I update state outside of React component?\nAbsolutely. You can even share store across multiple React instances (or if we're looking into future - across multiple frameworks).\n\nFor example:\n```tsx\nclass Timer extends Exome {\n  public time = 0\n\n  public increment() {\n    this.time += 1\n  }\n}\n\nconst timer = new Timer()\n\nsetInterval(timer.increment, 1000)\n\nfunction App() {\n  const { time } = useStore(timer)\n\n  return \u003ch1\u003e{time}\u003c/h1\u003e\n}\n```\n[__Open in Codesandbox__](https://codesandbox.io/s/exome-middleware-ro6of?file=/src/App.tsx)\n\n# IE support\nTo run Exome on IE, you must have `Symbol` and `Promise` polyfills and down-transpile to ES5 as usual. And that's it!\n\n# Motivation\nI stumbled upon a need to store deeply nested store and manage chunks of them individually and regular flux selector/action architecture just didn't make much sense anymore. So I started to prototype what would ideal deeply nested store interaction look like and I saw that we could simply use classes for this.\n\n**Goals I set for this project:**\n\n- [x] Easy usage with deeply nested state chunks (array in array)\n- [x] Type safe with TypeScript\n- [x] To have actions be only way of editing state\n- [x] To have effects trigger extra actions\n- [x] Redux devtool support\n\n# License\n[MIT](LICENCE) \u0026copy; [Marcis Bergmanis](https://twitter.com/marcisbee)\n","funding_links":[],"categories":["State Libraries","TypeScript","State Management","Uncategorized","Components \u0026 Libraries"],"sub_categories":["Mobile","Other State Libraries","Uncategorized","Utilities"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMarcisbee%2Fexome","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FMarcisbee%2Fexome","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMarcisbee%2Fexome/lists"}