{"id":13394070,"url":"https://github.com/storeon/storeon","last_synced_at":"2025-04-23T20:56:50.802Z","repository":{"id":41145742,"uuid":"175927747","full_name":"storeon/storeon","owner":"storeon","description":"🌩 A tiny (185 bytes) event-based Redux-like state manager for React, Preact, Angular, Vue, and Svelte","archived":false,"fork":false,"pushed_at":"2024-12-10T17:42:24.000Z","size":3054,"stargazers_count":1974,"open_issues_count":15,"forks_count":68,"subscribers_count":19,"default_branch":"main","last_synced_at":"2025-04-23T20:56:42.658Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://evilmartians.com/chronicles/storeon-redux-in-173-bytes","language":"JavaScript","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/storeon.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"ai"}},"created_at":"2019-03-16T04:52:02.000Z","updated_at":"2025-04-22T08:13:36.000Z","dependencies_parsed_at":"2025-01-13T01:00:29.876Z","dependency_job_id":"22ef61cf-413b-4bb2-bfa2-89d748467043","html_url":"https://github.com/storeon/storeon","commit_stats":{"total_commits":337,"total_committers":39,"mean_commits":8.64102564102564,"dds":"0.27002967359050445","last_synced_commit":"f9582effa862de1a2ad361cbe16b8de0f41616a5"},"previous_names":[],"tags_count":43,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/storeon%2Fstoreon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/storeon%2Fstoreon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/storeon%2Fstoreon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/storeon%2Fstoreon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/storeon","download_url":"https://codeload.github.com/storeon/storeon/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250514767,"owners_count":21443208,"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":[],"created_at":"2024-07-30T17:01:07.927Z","updated_at":"2025-04-23T20:56:50.782Z","avatar_url":"https://github.com/storeon.png","language":"JavaScript","funding_links":["https://github.com/sponsors/ai"],"categories":["JavaScript","cross framework","\u003e 1k ★","Extensions/Libraries","List","State Managers"],"sub_categories":[],"readme":"# Storeon\n\n*Deprecated in favor of [Nano Stores](https://github.com/nanostores/nanostores).*\n\n\u003cimg src=\"https://storeon.github.io/storeon/logo.svg\" align=\"right\"\n     alt=\"Storeon logo by Anton Lovchikov\" width=\"160\" height=\"142\"\u003e\n\nA tiny event-based Redux-like state manager for **React**, **Preact**,\n**[Angular]**, **[Vue]** and **[Svelte]**.\n\n* **Small.** 180 bytes (minified and gzipped). No dependencies.\n  It uses [Size Limit] to control size.\n* **Fast.** It tracks what parts of state were changed and re-renders\n  only components based on the changes.\n* **Hooks.** The same Redux reducers.\n* **Modular.** API created to move business logic away from React components.\n\nRead more about Storeon features in **[our article]**.\n\n```js\nimport { createStoreon } from 'storeon'\n\n// Initial state, reducers and business logic are packed in independent modules\nlet count = store =\u003e {\n  // Initial state\n  store.on('@init', () =\u003e ({ count: 0 }))\n  // Reducers returns only changed part of the state\n  store.on('inc', ({ count }) =\u003e ({ count: count + 1 }))\n}\n\nexport const store = createStoreon([count])\n```\n\n```js\nimport { useStoreon } from 'storeon/react' // or storeon/preact\n\nexport const Counter = () =\u003e {\n  // Counter will be re-render only on `state.count` changes\n  const { dispatch, count } = useStoreon('count')\n  return \u003cbutton onClick={() =\u003e dispatch('inc')}\u003e{count}\u003c/button\u003e\n}\n```\n\n```js\nimport { StoreContext } from 'storeon/react'\n\nrender(\n  \u003cStoreContext.Provider value={store}\u003e\n    \u003cCounter /\u003e\n  \u003c/StoreContext.Provider\u003e,\n  document.body\n)\n```\n\n[our article]: https://evilmartians.com/chronicles/storeon-redux-in-173-bytes\n[Size Limit]: https://github.com/ai/size-limit\n[Angular]: https://github.com/storeon/angular\n[Svelte]: https://github.com/storeon/svelte\n[Vue]: https://github.com/storeon/vue\n\n\u003ca href=\"https://evilmartians.com/?utm_source=storeon\"\u003e\n  \u003cimg src=\"https://evilmartians.com/badges/sponsored-by-evil-martians.svg\"\n       alt=\"Sponsored by Evil Martians\" width=\"236\" height=\"54\"\u003e\n\u003c/a\u003e\n\n\n## Tools\n\n* [`@storeon/router`](https://github.com/storeon/router)\n  tracks links and Back button click and allows you to open\n  pages without reloading the whole page.\n* [`@storeon/localstorage`](https://github.com/storeon/localstorage)\n  saves and restores state to `localStorage` or `sessionStorage`.\n* [`@storeon/crosstab`](https://github.com/storeon/crosstab)\n  synchronizes events between browser tabs.\n* [`@storeon/undo`](https://github.com/storeon/undo)\n  allows undoing or redoing the latest event.\n* [`@storeon/websocket`](https://github.com/storeon/websocket)\n  to sync actions through WebSocket.\n\nThird-party tools:\n\n* [`majo44/storeon-async-router`](https://github.com/majo44/storeon-async-router)\n  is router with data prefetch, modules lazy load, navigation cancellation,\n  and routes modification on the fly.\n* [`mariosant/storeon-streams`](https://github.com/mariosant/storeon-streams)\n  is side effects management library.\n* [`octav47/storeonize`](https://github.com/octav47/storeonize)\n  is migrating tool from Redux to Storeon.\n\n## Install\n\n```sh\nnpm install storeon\n```\n\nIf you need to support IE, you need to [compile `node_modules`] with Babel and\nadd [`Object.assign`] polyfill to your bundle. You should have this polyfill\nalready if you are using React.\n\n```js\nimport assign from 'object-assign'\nObject.assign = assign\n```\n\n[compile `node_modules`]: https://developer.epages.com/blog/coding/how-to-transpile-node-modules-with-babel-and-webpack-in-a-monorepo/\n[`Object.assign`]: https://www.npmjs.com/package/object-assign\n\n\n## Store\n\nThe store should be created with the `createStoreon()` function. It accepts a list\nof functions.\n\nEach function should accept a `store` as the only argument and bind their event listeners using `store.on()`.\n\n```js\n// store/index.js\nimport { createStoreon } from 'storeon'\n\nimport { projects } from './projects'\nimport { users } from './users'\n\nexport const store = createStoreon([projects, users])\n```\n\n```js\n// store/projects.js\n\nexport function projects (store) {\n  store.on('@init', () =\u003e ({ projects: [] }))\n\n  store.on('projects/add', ({ projects }, project) =\u003e {\n    return { projects: projects.concat([project]) }\n  })\n}\n```\n\nThe store has 3 methods:\n\n* `store.get()` will return current state. The state is always an object.\n* `store.on(event, callback)` will add an event listener.\n* `store.dispatch(event, data)` will emit an event with optional data.\n\n\n## Events\n\nThere are three built-in events:\n\n* `@init` will be fired in `createStoreon`. Bind to this event to set the initial state.\n* `@dispatch` will be fired on every new action (on `store.dispatch()` calls\n  and `@changed` events). It receives an array with the event name\n  and the event’s data. Can be useful for debugging.\n* `@changed` will be fired when any event changes the state.\n  It receives object with state changes.\n\nTo add an event listener, call `store.on()` with the event name and a callback function.\n\n```js\nstore.on('@dispatch', (state, [event, data]) =\u003e {\n  console.log(`Storeon: ${ event } with `, data)\n})\n```\n\n`store.on()` will return a cleanup function. Calling this function will remove\nthe event listener.\n\n```js\nconst unbind = store.on('@changed', …)\nunbind()\n```\n\nYou can dispatch any other events. Just do not start event names with `@`.\n\nIf the event listener returns an object, this object will update the state.\nYou do not need to return the whole state, return an object\nwith changed keys.\n\n```js\n// users: {} will be added to state on initialization\nstore.on('@init', () =\u003e ({ users:  { } }))\n```\n\nAn event listener accepts the current state as the first argument,\noptional event object as the second and optional store object as the third.\n\nSo event listeners can be reducers as well. As in Redux’s reducers,\nyour should change immutable.\n\n```js\nstore.on('users/save', ({ users }, user) =\u003e {\n  return {\n    users: { ...users, [user.id]: user }\n  }\n})\n\nstore.dispatch('users/save', { id: 1, name: 'Ivan' })\n```\n\nYou can dispatch other events in event listeners. It can be useful for async\noperations.\n\n```js\nstore.on('users/add', async (state, user) =\u003e {\n  try {\n    await api.addUser(user)\n    store.dispatch('users/save', user)\n  } catch (e) {\n    store.dispatch('errors/server-error')\n  }\n})\n```\n\n\n## Components\n\nFor functional components, the `useStoreon` hook will be the best option:\n\n```js\nimport { useStoreon } from 'storeon/react' // Use 'storeon/preact' for Preact\n\nconst Users = () =\u003e {\n  const { dispatch, users, projects } = useStoreon('users', 'projects')\n  const onAdd = useCallback(user =\u003e {\n    dispatch('users/add', user)\n  })\n  return \u003cdiv\u003e\n    {users.map(user =\u003e \u003cUser key={user.id} user={user} projects={projects} /\u003e)}\n    \u003cNewUser onAdd={onAdd} /\u003e\n  \u003c/div\u003e\n}\n```\n\nFor class components, you can use the `connectStoreon()` decorator.\n\n```js\nimport { connectStoreon } from 'storeon/react' // Use 'storeon/preact' for Preact\n\nclass Users extends React.Component {\n  onAdd = () =\u003e {\n    this.props.dispatch('users/add', user)\n  }\n  render () {\n    return \u003cdiv\u003e\n      {this.props.users.map(user =\u003e \u003cUser key={user.id} user={user} /\u003e)}\n      \u003cNewUser onAdd={this.onAdd} /\u003e\n    \u003c/div\u003e\n  }\n}\n\nexport default connectStoreon('users', 'anotherStateKey', Users)\n```\n\n`useStoreon` hook and `connectStoreon()` accept the list of state keys to pass\ninto `props`. It will re-render only if this keys will be changed.\n\n\n## DevTools\n\nStoreon supports debugging with [Redux DevTools Extension].\n\n```js\nimport { storeonDevtools } from 'storeon/devtools';\n\nconst store = createStoreon([\n  …\n  process.env.NODE_ENV !== 'production' \u0026\u0026 storeonDevtools\n])\n```\n\nDevTools will also warn you about **typo in event name**. It will throw an error\nif you are dispatching event, but nobody subscribed to it.\n\nOr if you want to print events to `console` you can use the built-in logger.\nIt could be useful for simple cases or to investigate issues in error trackers.\n\n```js\nimport { storeonLogger } from 'storeon/devtools';\n\nconst store = createStoreon([\n  …\n  process.env.NODE_ENV !== 'production' \u0026\u0026 storeonLogger\n])\n```\n\n[Redux DevTools Extension]: https://github.com/zalmoxisus/redux-devtools-extension\n\n\n## TypeScript\n\nStoreon delivers TypeScript declarations which allows to declare type\nof state and optionally declare types of events and parameter.\n\nIf a Storeon store has to be fully type safe the event types declaration\ninterface has to be delivered as second type to `createStore` function.\n\n```typescript\nimport { createStoreon, StoreonModule } from 'storeon'\nimport { useStoreon } from 'storeon/react' // or storeon/preact\n\n// State structure\ninterface State {\n  counter: number\n}\n\n// Events declaration: map of event names to type of event data\ninterface Events {\n  // `inc` event which do not goes with any data\n  'inc': undefined\n  // `set` event which goes with number as data\n  'set': number\n}\n\nconst counterModule: StoreonModule\u003cState, Events\u003e = store =\u003e {\n  store.on('@init', () =\u003e ({ counter: 0}))\n  store.on('inc', state =\u003e ({ counter: state.counter + 1}))\n  store.on('set', (state, event) =\u003e ({ counter: event}))\n}\n\nconst store = createStoreon\u003cState, Events\u003e([counterModule])\n\nconst Counter = () =\u003e {\n  const { dispatch, count } = useStoreon\u003cState, Events\u003e('count')\n  // Correct call\n  dispatch('set', 100)\n  // Compilation error: `set` event do not expect string data\n  dispatch('set', \"100\")\n  …\n}\n\n// Correct calls:\nstore.dispatch('set', 100)\nstore.dispatch('inc')\n\n// Compilation errors:\nstore.dispatch('inc', 100)   // `inc` doesn’t have data\nstore.dispatch('set', \"100\") // `set` event do not expect string data\nstore.dispatch('dec')        // Unknown event\n```\n\nIn order to work properly for imports, consider adding\n`allowSyntheticDefaultImports: true` to `tsconfig.json`.\n\n## Server-Side Rendering\n\nIn order to preload data for server-side rendering, Storeon provides the\n`customContext` function to create your own `useStoreon` hooks that\ndepend on your custom context.\n\n```js\n// store.jsx\nimport { createContext, render } from 'react' // or preact\n\nimport { createStoreon, StoreonModule } from 'storeon'\nimport { customContext } from 'storeon/react' // or storeon/preact\n\nconst store = …\n\nconst CustomContext = createContext(store)\n\n// useStoreon will automatically recognize your storeon store and event types\nexport const useStoreon = customContext(CustomContext)\n\nrender(\n  \u003cCustomContext.Provider value={store}\u003e\n    \u003cCounter /\u003e\n  \u003c/CustomContext.Provider\u003e,\n  document.body\n)\n```\n\n```js\n// children.jsx\nimport { useStoreon } from '../store'\n\nconst Counter = () =\u003e {\n  const { dispatch, count } = useStoreon('count')\n\n  dispatch('set', 100)\n  …\n}\n```\n\n\n## Testing\n\nTests for store can be written in this way:\n\n```js\nit('creates users', () =\u003e {\n  let addUserResolve\n  jest.spyOn(api, 'addUser').mockImplementation(() =\u003e new Promise(resolve =\u003e {\n    addUserResolve = resolve\n  }))\n  let store = createStoreon([usersModule])\n\n  store.dispatch('users/add', { name: 'User' })\n  expect(api.addUser).toHaveBeenCalledWith({ name: 'User' })\n  expect(store.get().users).toEqual([])\n\n  addUserResolve()\n  expect(store.get().users).toEqual([{ name: 'User' }])\n})\n```\n\nWe recommend to keep business logic away from components. In this case,\nUI kit (special page with all your components in all states)\nwill be the best way to test components.\n\nFor instance, with [UIBook] you can mock store and show notification\non any `dispatch` call.\n\n[UIBook]: https://github.com/vrizo/uibook/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstoreon%2Fstoreon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstoreon%2Fstoreon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstoreon%2Fstoreon/lists"}