{"id":15376708,"url":"https://github.com/bennypowers/state","last_synced_at":"2025-04-15T16:54:17.867Z","repository":{"id":43995207,"uuid":"238503394","full_name":"bennypowers/state","owner":"bennypowers","description":"Lazy, explicit, typed, 1kb one-way state management","archived":false,"fork":false,"pushed_at":"2022-02-13T01:02:15.000Z","size":293,"stargazers_count":16,"open_issues_count":4,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-23T12:55:47.056Z","etag":null,"topics":["explicit-effects","state","web-components"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bennypowers.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":["bennypowers"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"custom":null}},"created_at":"2020-02-05T17:03:11.000Z","updated_at":"2024-11-27T21:21:13.000Z","dependencies_parsed_at":"2022-09-05T21:40:33.968Z","dependency_job_id":null,"html_url":"https://github.com/bennypowers/state","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bennypowers%2Fstate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bennypowers%2Fstate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bennypowers%2Fstate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bennypowers%2Fstate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bennypowers","download_url":"https://codeload.github.com/bennypowers/state/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241404415,"owners_count":19957655,"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":["explicit-effects","state","web-components"],"created_at":"2024-10-01T14:08:43.713Z","updated_at":"2025-03-03T04:30:44.716Z","avatar_url":"https://github.com/bennypowers.png","language":"TypeScript","funding_links":["https://github.com/sponsors/bennypowers"],"categories":[],"sub_categories":[],"readme":"# @pwrs/state\n\nA lazy, explicit, typed, 1kb one-way state management library.\n\n- 👌 [Itsy *Bitsy*](#itsy-bitsy)\n- 👼 [Born *Lazy*](#born-lazy)\n- ⛲️ [Unidirectional *State*](#unidirectional-state)\n- 🎇 [With Explicit *Effects*](#explicit-effects)\n- ⌨️ [That's Well *Typed*](#well-typed)\n\n## Itsy Bitsy\n\n1kb is not a tremendous amount over the wire.\n\n## Born Lazy\nimport the `registerState` function to declare an initial state for your app.\n\nRegister state takes an object of state keys, which hold an object of state and optionally effects.\n\nWhat that means is that if you want your initial state to look like this:\n```js\n{\n  auth: { user: null }\n  router: { page: 'home' }\n}\n```\n\nThen call `registerState` like so:\n```js\n// main.js, our app's entrypoint\nimport { registerState } from '@pwrs/state';\n\n// initial app state. eagerly loaded\nregisterState({\n  auth: {\n    state: { user: null }\n  },\n  router: {\n    state: { page: 'home' }\n  }\n});\n```\n\nYou can declare the entire initial state of you app in one go in this way, but it's much better to declare your state *lazily* by putting your calls to `registerState` in dynamically-imported modules. That way, you don't have to load the state management code for a page until the user navigates to it.\n\nBy calling `registerState` in dynamically-imported modules, you can register their slice of state when you load the module, instead of having to know in advance.\n\nThis also lets you keep your component state close by to their element definitions. That might make it easier to focus on the task at hand. For example, this `app-home` component definition is lazily-loaded along with its state.\n\n```js\n// app-home/app-home.js\nimport { StateElement, registerState, customElement } from '@pwrs/state';\n\nregisterState({\n  home: {\n    state: { selected: null },\n  }\n})\n\n@customElement('app-home')\nclass AppHome extends StateElement {/*..*/}\n```\n\n## Unidirectional State\n\n`state` is a read-only property on `StatefulElement`s. In order to affect change in your app's state, call the `updateState` function with a partial representation of the new state.\n\n```js\n// app-edit-profile/app-edit-profile.js\nimport '@material/mwc-textfield';\nimport { customElement, html } from 'lit-element';\nimport { registerState, StateElement, updateState } from '@pwrs/state';\n\nregisterState({\n  editProfile: {\n    state: {\n      name: null,\n      picture: null,\n    },\n  }\n})\n\n@customElement('app-edit-profile') class AppEditProfile extends StateElement {\n  render() {\n    if (!user)\n      return html`\u003ca href=\"/login\"\u003eLogin\u003c/a\u003e`\n    else\n      return html`\n        \u003ch2\u003eEdit Your Profile, ${(this.state.auth?.user?.name ?? 'Friend!')}\u003c/h2\u003e\n\n        \u003cmwc-textfield label=\"Name\" @change=\"${this.onChange}\"\n            .value=\"${(this.state.auth?.user?.name ?? '')}\"\n        \u003e\u003c/mwc-textfield\u003e\n\n        \u003cmwc-textfield label=\"Picture\" @change=\"${this.onChange}\"\n            .value=\"${(this.state.auth?.user?.picture ?? '')}\"\n        \u003e\u003c/mwc-textfield\u003e\n      `;\n  }\n\n  onChange({ target: { label, value } }) {\n    updateState({ editProfile: { [label.toLowerCase()]: value } });\n  }\n}\n```\n\n## Explicit Effects\n\nEffects are functions that run whenever the state changes. Register effects by\npassing an `effects` property to your slice' initializer.\n\nEffects are ternary functions that take their next local state slice, the previous local state slice, and the global state. They have no return value. They run *after* the new state is assigned.\n\n### Simple Effects\nSimple effects perform some task every time the state changes. This example shows how a simple router set up can register an effect which lazy-loads page components.\n\n```js\nimport { registerState, updateState } from '@pwrs/state';\n\nconst lazyLoad = ({ page }) =\u003e page \u0026\u0026 import(`../app-${page}/app-${page}.js`),\n\nregisterState({\n  router: {\n    state: { page: null },\n    effects: lazyLoad\n  }\n});\n```\n\n### Declarative Effects\n\nMost effects you write will have some predicate that drives their behaviour. We've provided some logic functions to help you write declarative effects\n\n```js\n// app-checkout/app-checkout.js\nimport { registerState, updateState } from '@pwrs/state';\nimport { all, hasProp, none, when } from '@pwrs/state/logic';\n\nasync function paymentEffect({ cardToken, productId }) {\n  const { data: response, error } = await postCharge({ cardToken, productId });\n  updateState({ subscribe: { response, error } });\n}\n\nconst canPay = all(\n  none(hasProp('inFlight'), hasProp('response'), hasProp('error')),\n  hasProp('cardToken'),\n  hasProductId,\n)\n\nconst effects = when(canPay, paymentEffect);\n\nconst state = {\n  error: null,\n  response: null,\n  cardToken: null,\n  productId: null,\n  inFlight: false\n};\n\nregisterState({ subscribe: { effects, state } });\n```\n\nYou can stack these up as well in an array of effects functions:\n\n```js\nconst initialSearchParams = new URLSearchParams(location.search);\n\nregisterState({\n  router: {\n    state: { hash: '', page: 'home', searchParams: initialSearchParams },\n    effects: [\n      lazyLoadPages,\n      when(hasDocumentIdParam, updateDocumentState),\n      when(isHomePage, scrollHomeToHash),\n      when(isHomePage, startScrollSpy),\n      when(not(isHomePage), disconnectScrollSpy),\n      when(and(isAccountPage, isUserLoggedIn), fetchProfile),\n    ],\n  },\n});\n```\n\nThese helpers were heavily inspired by [crocks](https://crocks.dev). Go check them out.\n\n#### Debugging with `trace()`\n\nimport `trace` to debug your state:\n\n```js\nimport { registerState, trace } from '@pwrs/state';\n\nregisterState({\n  launchControl: {\n    effects: when(isUnderAttack, trace('Under Attack?')),\n    state: { misslesIncoming: false, launchCoordinates: null }\n  }\n});\n```\n\n`trace` will log the message you provide it, followed by the contents of the state slice.\n\n## Well-Typed\n\n`@pwrs/state` gets its types from the `State` interface on the `@pwrs/state/state` module. Use Typescript's [Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to merge your app's state in.\n\nIf you're not writing typescript you can still benefit by putting those declarations in an \"ambient\" declaration file.\n\n```ts\n// app-checkout/app-checkout.ts\nimport type { PaymentBros } from '@types/payment-bros';\nimport type { PaymentResponse, PaymentError } from '../server/typings'\nimport { registerState } from '@pwrs/state';\n\ninterface CheckoutState {\n  token: PaymentBros.Token;\n  response: PaymentResponse;\n  error: Error | PaymentError;\n}\n\ndeclare module '@pwrs/state/state' {\n  interface State {\n    checkout: CheckoutState;\n  }\n}\n\nregisterState({\n  checkout: {\n    state: { token: null, response: null, error: null }\n  }\n})\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbennypowers%2Fstate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbennypowers%2Fstate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbennypowers%2Fstate/lists"}