{"id":17929143,"url":"https://github.com/skarab42/riux","last_synced_at":"2025-06-30T08:34:07.161Z","repository":{"id":56718702,"uuid":"523442272","full_name":"skarab42/riux","owner":"skarab42","description":"📦 Fully typed and immutable store made on top of Immer with mutation, action, subscription and validation!","archived":false,"fork":false,"pushed_at":"2023-10-07T06:13:54.000Z","size":445,"stargazers_count":9,"open_issues_count":4,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-18T21:09:25.879Z","etag":null,"topics":["immer","immutable","redux","state","store","typed","validation","zod"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/riux","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/skarab42.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}},"created_at":"2022-08-10T17:47:11.000Z","updated_at":"2023-09-15T10:50:40.000Z","dependencies_parsed_at":"2022-08-16T00:10:39.689Z","dependency_job_id":null,"html_url":"https://github.com/skarab42/riux","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skarab42%2Friux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skarab42%2Friux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skarab42%2Friux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skarab42%2Friux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skarab42","download_url":"https://codeload.github.com/skarab42/riux/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245210848,"owners_count":20578299,"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":["immer","immutable","redux","state","store","typed","validation","zod"],"created_at":"2024-10-28T21:07:46.197Z","updated_at":"2025-03-24T04:30:55.244Z","avatar_url":"https://github.com/skarab42.png","language":"TypeScript","funding_links":["https://github.com/sponsors/skarab42"],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://user-images.githubusercontent.com/62928763/184168494-85d90d39-66d1-4f5a-a484-d4ae464e8495.svg\" alt=\"Riux logo\" /\u003e\n\u003c/div\u003e\n\n\u003ch1 align=\"center\"\u003e\n  Riux is a fully typed and \u003ca href=\"#immutability\"\u003eimmutable\u003c/a\u003e store made on top of \u003ca href=\"https://immerjs.github.io/immer/\"\u003eImmer\u003c/a\u003e with \u003ca href=\"#add-some-mutations\"\u003emutation\u003c/a\u003e, \u003ca href=\"#add-some-actions\"\u003eaction\u003c/a\u003e, \u003ca href=\"#add-some-subscriptions\"\u003esubscription\u003c/a\u003e and \u003ca href=\"#validation-with-zod-superstruct-yup-tson\"\u003evalidation\u003c/a\u003e!\n  \u003cbr /\u003e\n\u003c/h1\u003e\n\n\u003cbr /\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003ca href=\"https://github.com/skarab42/riux/actions/workflows/test-release.yaml\"\u003e\u003cimg src=\"https://github.com/skarab42/riux/actions/workflows/test-release.yaml/badge.svg\" alt=\"Test and Lint\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://codecov.io/gh/skarab42/riux\"\u003e\u003cimg src=\"https://codecov.io/gh/skarab42/riux/branch/main/graph/badge.svg?token=YVUTZ0C1GR\"/\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/github/languages/code-size/skarab42/riux?color=success\u0026style=flat\" alt=\"GitHub code size in bytes\"\u003e\n  \u003cimg src=\"https://img.shields.io/github/license/skarab42/riux?color=success\" alt=\"GitHub\"\u003e\n  \u003ca href=\"https://github.com/sponsors/skarab42\"\u003e\u003cimg src=\"https://img.shields.io/github/sponsors/skarab42?color=ff69b4\u0026label=%E2%9D%A4%20sponsors%20\" alt=\"GitHub Sponsors\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.twitch.tv/skarab42\"\u003e\u003cimg src=\"https://img.shields.io/twitch/status/skarab42?style=social\" alt=\"Twitch Status\"\u003e\u003c/a\u003e\n\u003c/div\u003e\n\n\u003cbr /\u003e\n\u003chr /\u003e\n\n\u003cdetails\u003e\n\u003csummary align=\"center\"\u003eTable of contents 👀\u003c/summary\u003e\n\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Basic usage](#basic-usage)\n  - [Add some mutations](#add-some-mutations)\n  - [Add some subscriptions](#add-some-subscriptions)\n  - [Add some actions](#add-some-actions)\n  - [Immutability](#immutability)\n  - [Validation with custom parser](#validation-with-custom-parser)\n  - [Validation with Zod, Superstruct, Yup, tson, ...](#validation-with-zod-superstruct-yup-tson-)\n- [Scaling](#scaling)\n- [Type inference](#type-inference)\n\n\u003c/details\u003e\n\n\u003chr /\u003e\n\u003cbr /\u003e\n\n# Installation\n\n```bash\npnpm add riux\n```\n\n# Usage\n\n### Basic usage\n\n```ts\nimport riux from 'riux';\n\nconst store = riux(42);\n\nconst reset = () =\u003e store.update(() =\u003e 0);\n// 'draft' is automatically typed as 'number'\nconst increment = () =\u003e store.update((draft) =\u003e draft + 1);\nconst add = (value: number) =\u003e store.update((draft) =\u003e draft + value);\n\nincrement(); // 1\nincrement(); // 2\nadd(40); // 42\nreset(); // 0\n```\n\nOk, that's cool, but that's more or less what [Immer](https://immerjs.github.io/immer/produce#example) already does! What next?\n\n### Add some mutations\n\nThis is the same example as above but using mutations.\n\n```ts\nconst store = riux(42, {\n  // 'draft' is automatically typed as 'number' in each mutation\n  mutations: {\n    reset: () =\u003e 0,\n    increment: (draft) =\u003e draft + 1,\n    add: (draft, value: number) =\u003e draft + value,\n  },\n});\n\nstore.mutation('increment'); // 1\nstore.mutation('increment'); // 2\nstore.mutation('add', 40); // 42\nstore.mutation('reset'); // 0\n```\n\nAs you can see the code is a bit longer but better structured. All is centralized in the store (which allows for example to have autocompletions in the IDE). But this is not the only advantage and you will see it later with the [actions](#add-some-actions).\n\nOk nice... But what next?\n\n### Add some subscriptions\n\nYou can register a function that will be called when the state is updated.\n\n\u003e An update is raised each time the `update`, `mutation` or `action` function is called.\n\n```ts\nconst store = riux({ counter: 0 });\n\n// 'state' is automatically typed as '{ counter: number }'\nconst subscription = store.subscribe((state) =\u003e {\n  console.log(state.counter);\n});\n```\n\n```ts\nsubscription.disable(); // unsubscribe\nsubscription.enable(); // resubscribe\n```\n\nWell, that's a minimum, isn't it? What else is there?\n\n### Add some actions\n\nThe actions allow you to compose mutations that do not raise an event before the end of the action.\n\n```ts\nconst store = riux(42, {\n  // 'draft' is automatically typed as 'number' in each mutation\n  mutations: {\n    reset: () =\u003e 0,\n    increment: (draft) =\u003e draft + 1,\n    add: (draft, value: number) =\u003e draft + value,\n  },\n  // 'mutation' is automatically typed in each action\n  actions: {\n    incrementTwice(mutation) {\n      mutation('increment');\n      mutation('increment');\n    },\n    addArray(mutation, values: number[]) {\n      for (const value of values) {\n        mutation('add', value);\n      }\n    },\n  },\n});\n\nstore.subscribe((counter) =\u003e {\n  console.log(counter);\n});\n\nstore.mutation('reset'); // 0\n\nstore.action('incrementTwice'); // 2\nstore.action('incrementTwice'); // 4\nstore.action('addArray', [5, 8, 10, 15]); // 42\n\n// 'console.log' is called 4 times!\n```\n\n\u003e If an error is raised in an action, it will not be finalised and the state will remain unchanged, even if any mutations have been successfully completed.\n\nIncredible, but I think I've seen this before! You talk about immutability at the beginning, and you haven't even mentioned it yet... say more!\n\n### Immutability\n\nBy default when you create a store, it becomes immutable as well as its source (as far as possible). This behaviour can be changed for the source but not for the state. In short, once created you will not be able to modify the state or any of is properties outside of the store!\n\n```ts\nconst source = { life: 42 };\nconst store = riux(source);\n\nsource.life = 1337; // RUNTIME ERROR: Cannot assign to read only property 'life' of object...\n\nconst initialState = store.initial();\nconst currentState = store.current();\n\ninitialState.life = 1337; // TS ERROR + RUNTIME ERROR: Cannot assign to 'life' because it is a read-only property.\ncurrentState.life = 1337; // TS ERROR + RUNTIME ERROR: Cannot assign to 'life' because it is a read-only property.\n```\n\nIf you wish to avoid the first error you can set the `freezeInitialState` option to `false`.\n\n\u003e Note that the state retrieved via `store.initial()` remains unchanged and immutable. Only the source outside the store will not be frozen.\n\n```ts\nconst store = riux(source, { freezeInitialState: false });\n\nsource.life = 1337; // NO ERROR!\n// ...\ninitialState.life = 1337; // TS ERROR + RUNTIME ERROR: Cannot assign to 'life' because it is a read-only property.\ncurrentState.life = 1337; // TS ERROR + RUNTIME ERROR: Cannot assign to 'life' because it is a read-only property.\n```\n\nOk that rocks, one source of truth I like that! But I want more!\n\n### Validation with custom parser\n\nBy default no data validation is done at runtime, only TypeScript protects you from input type errors at build time or in the IDE.\n\nIf you want to validate your data at runtime, you must provide a `parse` function that validates/casts the state before finalizing the draft and either returns a strongly typed value (if valid) or throws an error (if invalid).\n\n```ts\nconst store = riux(0, {\n  parse: (state) =\u003e {\n    if (typeof state === 'number') return state;\n    throw new Error(`expected 'number' got '${typeof state}'`);\n  },\n  mutations: {\n    add(draft, value: number) {\n      return draft + value;\n    },\n  },\n});\n\nstore.mutation('add', 'prout'); // TS ERROR + RUNTIME ERROR: expected 'number' got 'string'\n```\n\nAnother advantage of this method is the ability to specify multiple types (union). Imagine you have a single value that can be undefined or a string. How do you do this?\n\n```ts\nconst store = riux(undefined, {\n  parse: (state) =\u003e {\n    if (state === undefined || typeof state === 'string') return state;\n    throw new Error(`expected 'number' got '${typeof state}'`);\n  },\n  mutations: {\n    set(_draft, value: string | undefined) {\n      return value;\n    },\n  },\n});\n\nstore.current(); // string | undefined\n\nstore.mutation('set', undefined);\nstore.mutation('set', 'or string');\nstore.mutation('set', 42); // TS ERROR + RUNTIME ERROR: expected 'string' got 'number'\n```\n\nThis is starting to be very interesting, but I'm sure we can do better! Right?\n\n### Validation with Zod, Superstruct, Yup, tson, ...\n\n[Zod](https://github.com/colinhacks/zod) is a TypeScript-first schema validation with static type inference which can be used to validate the state of the store on each mutation. This is very powerful when it comes to parse complex data structures.\n\n```ts\nconst schema = z.object({\n  name: z.string().min(3),\n  life: z.number(),\n});\n\nconst store = riux(\n  { name: 'nyan', life: 42 },\n  {\n    parse: (state) =\u003e schema.parse(state),\n    mutations: {\n      setName(draft, name: string) {\n        draft.name = name;\n      },\n      setLife(draft, life: number) {\n        draft.life = life;\n      },\n    },\n  },\n);\n\nstore.current(); // { readonly name: string; readonly life: number }\n\nstore.mutation('setName', 'bob'); // { name: 'bob', life: 42 }\nstore.mutation('setLife', 1337); // { name: 'bob', life: 1337 }\n\nstore.mutation('setLife', [true]); // TS ERROR + RUNTIME ZodError: Expected number, received array\nstore.mutation('setName', 'na'); // TS ERROR + RUNTIME ZodError: String must contain at least 3 character(s)\n```\n\n\u003e The Example above is with [Zod](https://github.com/colinhacks/zod), but it can work with any library that exposes a function/method with the right signature, like [Superstruct](https://github.com/ianstormtaylor/superstruct), [Yup](https://github.com/jquense/yup), [tson](https://github.com/skarab42/tson) and more...\n\nNeed more?\n\n# Scaling\n\nAs your store grows you will probably want to split your code into several files. Here's how to do it.\n\n```ts\n// initial-state.ts\n\nexport const initialState = {\n  name: 'nyan',\n  life: 42,\n  items: [\n    { id: 1, name: 'item-1', rare: true },\n    { id: 2, name: 'item-2', rare: false },\n    { id: 3, name: 'item-3', rare: false },\n  ],\n};\n\nexport type State = typeof initialState;\nexport type Item = State['items'][number];\n```\n\n```ts\n// mutations.ts\n\nimport { createMutation, createMutations } from 'riux';\nimport { initialState, type Item } from './initial-state.js';\n\n// This function could be in another file, for example 'mutations/add-item.ts.\nconst addItem = createMutation(initialState, (draft, item: Item) =\u003e {\n  draft.items.push(item);\n});\n\nexport const mutations = createMutations(initialState, {\n  setName: (draft, name: string) =\u003e {\n    draft.name = name;\n  },\n  setLife(draft, life: number) {\n    draft.life = life;\n  },\n  addItem,\n});\n```\n\n```ts\n// actions/add-items.ts\n\nimport { createAction } from 'riux';\nimport { initialState, type Item } from '../initial-state.js';\nimport { mutations } from '../mutations.js';\n\nexport const addItems = createAction(initialState, mutations, (mutation, items: Item[]) =\u003e {\n  for (const item of items) {\n    mutation('addItem', item);\n  }\n});\n```\n\n```ts\n// actions.ts\n\nimport { createAction, createActions } from 'riux';\nimport { initialState, type Item } from './initial-state.js';\nimport { mutations } from './mutations.js';\nimport { addItems } from './actions/add-items.js';\n\nexport const actions = createActions(initialState, mutations, {\n  setName(mutation, name: string) {\n    mutation('setName', name);\n  },\n  addItems,\n});\n```\n\n```ts\n// store.ts\nimport { initialState, type Item } from './initial-state.js';\nimport { mutations } from './mutations.js';\nimport { actions } from './actions.js';\n\nexport const store = createStore(initialState, { mutations, actions });\n```\n\n# Type inference\n\nYou can extract the TypeScript types of any store with `InferState`, `InferMutations` or `InferActions`.\n\n```ts\nimport riux, { type InferState, type InferMutations } from 'riux';\n\nconst store = riux(42, {\n  mutations: {\n    add: (state, value: number) =\u003e state + value,\n  },\n});\n\ntype State = InferState\u003ctypeof store\u003e; // number\ntype Mutations = InferMutations\u003ctypeof store\u003e; // { add: (state: number, value: number) =\u003e number; }\n```\n\nOk that's all for now, but if you think something is missing you can open an [issue](https://github.com/skarab42/riux/issues) or even better make a [pull request](https://github.com/skarab42/riux/pulls).\n\n---\n\nScaffolded with [@skarab/skaffold](https://www.npmjs.com/package/@skarab/skaffold)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskarab42%2Friux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskarab42%2Friux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskarab42%2Friux/lists"}