{"id":23413147,"url":"https://github.com/tamb/substate","last_synced_at":"2025-08-02T12:06:42.313Z","repository":{"id":40838130,"uuid":"118075606","full_name":"tamb/substate","owner":"tamb","description":"pub/sub state management with optional deep cloning","archived":false,"fork":false,"pushed_at":"2024-12-30T04:52:16.000Z","size":1441,"stargazers_count":16,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-27T15:01:07.828Z","etag":null,"topics":["javascript","react","state","statemanager","substate"],"latest_commit_sha":null,"homepage":"","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/tamb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-01-19T04:09:25.000Z","updated_at":"2025-04-09T10:12:32.000Z","dependencies_parsed_at":"2024-06-21T15:33:48.397Z","dependency_job_id":"a76c1df8-a1f5-470b-828e-8fee73e974fe","html_url":"https://github.com/tamb/substate","commit_stats":null,"previous_names":["tomsaporito/substate"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/tamb/substate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tamb%2Fsubstate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tamb%2Fsubstate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tamb%2Fsubstate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tamb%2Fsubstate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tamb","download_url":"https://codeload.github.com/tamb/substate/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tamb%2Fsubstate/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268385732,"owners_count":24242101,"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","status":"online","status_checked_at":"2025-08-02T02:00:12.353Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["javascript","react","state","statemanager","substate"],"created_at":"2024-12-22T19:25:24.385Z","updated_at":"2025-08-02T12:06:42.265Z","avatar_url":"https://github.com/tamb.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ⛱️ substate\n\n## Automatically clone or deep clone your state\n\n## The Problem\n\nMost state management libraries don't clone your state for you.  \nMost don't optionally deep clone your state.\nMost don't offer an entire messaging system either.\nMost don't come as a 2kb gzipped package.\nThis one does.\n\n## Purpose\n\n- To manage state with a simple pub/sub pattern\n- To improve upon the pub/sub with unidirectional data flow\n- For State to return a new state (pure function)\n- Message filtering can be applied _without_ a `switch` statement (you create your own event `$type`)\n- To allow for manipulation of deeply nested state properties through use of strings `{'my[index]deeply.nests.state': 'new value'}` (we're sending this to substate to _not mutate_ the state, but make a new copy (Flux-y)!\n- Maintain a small size\n\n## Terms\n\n### store\n\nThe `store` is the substate instance. It has methods and state storage. It basically handles all your changes for you and acts as a mediator between different parts of your application. It's really a simple pub/sub pattern with data in it. That's all.\n\n```js\nstore - \"I'll handle this!\"\n  ___________________\n |                   |\n | message queues    |\n | application state |\n |___________________|\n```\n\n### emit\n\nA method that shoots a `$type` and `payload` to the `store`.\nThis method tells the `store`:\n\"Hey store. I need you to send this message `$type` out. And here's a `payload` of data to send with it!\"\n\n```js\nstore.emit($type, payload);\n```\n\n### on\n\nA method that listens for the above `$type` and fires a callback function that gets passed the `emit` methods `payload`\n\"Hello store. When you send out a message of this `$type`, please fire this `callbackFunction` and pass it your `payload`! Thanks!\"\n\n```js\nstore.on($type, callbackFunction);\n```\n\n### off\n\nA method that stops a certain `callbackFunction` on a specific `$type`\n\"Howdy store. When you send out a message of this `$type`, you don't need to fire this `callbackFunction`. Please remove the function from your queue.\"\n\n```js\nstore.off($type, callbackFunction);\n```\n\n### payload\n\nAn object of data. You can put any data in there that you want. The idea is that you would put your updated `state` object in there. The `store` will save your old state and `emit` your make updates to your new state according to this object. When triggering a state change with `UPDATE_STATE` you have the option of passing 2 fields into your `payload`\n\n- `$type` - this is a String value of a message `$type` that the `store` will `emit`. So if you pass it `$type: \"SAY_HI\"`, the `store` will emit `store.emit(\"SAY_HI\", data)` and any callbacks that have been registered with `store.on(\"SAY_HI\", callback)` will be fired _in registration order_.\n- `$deep` - this is a boolean that, when set to `true` will deep clone your state object. In the guts of the store we use `Object.assign`, which does not deep clone the state object. But the store has a special trick that can deep clone for you. So that means you don't have to normalize your state. You can have it as nested and complicated as you want. This is a huge plus for people who want their state to reflect their complex dataset.\n\n## How it Works\n\n### The Steps\n\n1. (if using modules)\n\n```js\n//store.js\nimport Substate from \"substate\";\n\nexport const store = new Substate({\n  name: \"storeExample\",\n  defaultDeep: true,\n  afterUpdate: [myMiddleware],\n  beforeUpdate: [myBeforeMiddleware],\n  state: {\n    todos: [],\n  },\n});\n\n// MyComponent.js\nimport { store } from \"./store.js\";\n```\n\n2. Components will register one or more methods to rerender themselves using your instance (see [instantiation](#instantiation))\n   using `myInstance.on('STATE_UPDATED', rerender)` per method\n   You can register to a custom event as well\n\n```js\n// MyComponent.js\n\n// default state event\nstore.on(\"STATE_UPDATED\", rerenderFunction);\n\n// custom state event\nstore.on(\"HEIGHT_CHANGE\", rerenderFunction);\n```\n\n3. Components take UI event (\"click\", \"focus\", etc) and pass it off to a Handler/Reducer\n\n```js\n// MyComponent.js\nelement.addEventListener(\"click\", clickHandler);\n```\n\n4. The Handler/Reducer figures out what should change in the state (it does not update the state directly). It also figures out if/what `$type` should be sent to the Pub/Sub module\n\n```js\n// MyComponent.js\n\nclickHandler = () =\u003e {\n  // define which fields should be updated and to what values\n  // you can use string notation because of the underlying technology in substate!\n\n  const newState = {\n    name: \"Pablo\",\n    \"height.inches\": 62,\n    \"height.centimeters\": 157.48,\n    //$type: \"HEIGHT_CHANGE\", -- tells the store to emit a custom event when the state is updated\n    //$deep: true, -- tells the store to deep clone the state\n  };\n\n  store.updateState(newState);\n  // OR use the UPDATE_STATE event\n  store.emit(\"UPDATE_STATE\", newState);\n  // OR use a custom event\n};\n```\n\n**A couple notes:**\nIf you want a deep clone pass in `$deep: true` into the state on emit. OR `defaultDeep: true` in the options.\n\n```js\nconst newState = {\n  ...newValues,\n  $deep: true,\n};\n```\n\nIf you want to emit a custom event, you can pass in a `$type`\nIf you want a deep clone pass in `$deep: true` into the state on emit. OR `defaultDeep: true` in the options.\n\n```js\nconst newState = {\n  ...newValues,\n  $type: \"HEIGHT_CHANGE\",\n};\n```\n\n## Interfaces\n\n### IPubSub\n\n```ts\ninterface IPubSub {\n  events: IEvents; //Holds the events and their listeners\n  on(eventName: string, fn: Function): void; //Adds a listener to an event\n  off(eventName: string, fn: Function): void; //Removes a listener from an event\n  removeAll(): void; //Removes all listeners from all events\n  removeAllOf(eventName: string): void; //Removes all listeners from a specific event\n  emit(eventName: string, data: object): void; //Emits an event with data\n}\n```\n\n### ISubstate\n\nThe Substate instance is a pub/sub pattern with a state storage. It has methods and state storage. It basically handles all your changes for you and acts as a mediator between different parts of your application. It's really a simple pub/sub pattern with data in it. That's all.\n\n```ts\nexport interface ISubstate extends IPubSub {\n  name: string; // name of the instance\n  afterUpdate: Function[] | []; // array of functions to be called after state update\n  beforeUpdate: Function[] | []; // array of functions to be called before state update\n  currentState: number; // index of the current state\n  stateStorage: IState[]; // array of states\n  defaultDeep: boolean; // default deep clone setting\n  getState(index: number): {}; // get state by index\n  getCurrentState(): IState; // get current state\n  getProp(prop: string): any; // get property from current state\n  resetState(): void; // reset state to initial state\n  updateState(action: IState): void; // update state with action\n}\n```\n\n### IConfig\n\nThe configuration object for the Substate instance. You pass is into the Substate instance\n\n```ts\nexport interface IConfig {\n  name?: string;\n  afterUpdate?: Function[] | [];\n  beforeUpdate?: Function[] | [];\n  currentState?: number;\n  stateStorage?: IState[];\n  defaultDeep?: boolean;\n  state?: object;\n}\n```\n\n### IState\n\n```ts\ninterface IState {\n  [key: string]: any;\n  $type?: string;\n  $deep?: boolean;\n}\n```\n\n### IEvents\n\n```ts\ninterface IEvents {\n  [id: string]: Function[];\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftamb%2Fsubstate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftamb%2Fsubstate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftamb%2Fsubstate/lists"}