๐Ÿ“ฆ Fully typed and immutable store made on top of Immer with mutation, action, subscription and validation!

Overview
Riux logo

Riux is a fully typed and immutable store made on top of Immer with mutation, action, subscription and validation!


Test and Lint GitHub code size in bytes GitHub GitHub Sponsors Twitch Status


Table of contents ๐Ÿ‘€


Installation

pnpm add riux

Usage

Basic usage

import riux from 'riux';

const store = riux(42);

const reset = () => store.update(() => 0);
// 'draft' is automatically typed as 'number'
const increment = () => store.update((draft) => draft + 1);
const add = (value: number) => store.update((draft) => draft + value);

increment(); // 1
increment(); // 2
add(40); // 42
reset(); // 0

Ok, that's cool, but that's more or less what Immer already does! What next?

Add some mutations

This is the same example as above but using mutations.

const store = riux(42, {
  // 'draft' is automatically typed as 'number' in each mutation
  mutations: {
    reset: () => 0,
    increment: (draft) => draft + 1,
    add: (draft, value: number) => draft + value,
  },
});

store.mutation('increment'); // 1
store.mutation('increment'); // 2
store.mutation('add', 40); // 42
store.mutation('reset'); // 0

As 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.

Ok nice... But what next?

Add some subscriptions

You can register a function that will be called when the state is updated.

An update is raised each time the update, mutation or action function is called.

const store = riux({ counter: 0 });

// 'state' is automatically typed as '{ counter: number }'
const subscription = store.subscribe((state) => {
  console.log(state.counter);
});
subscription.disable(); // unsubscribe
subscription.enable(); // resubscribe

Well, that's a minimum, isn't it? What else is there?

Add some actions

The actions allow you to compose mutations that do not raise an event before the end of the action.

const store = riux(42, {
  // 'draft' is automatically typed as 'number' in each mutation
  mutations: {
    reset: () => 0,
    increment: (draft) => draft + 1,
    add: (draft, value: number) => draft + value,
  },
  // 'mutation' is automatically typed in each action
  actions: {
    incrementTwice(mutation) {
      mutation('increment');
      mutation('increment');
    },
    addArray(mutation, values: number[]) {
      for (const value of values) {
        mutation('add', value);
      }
    },
  },
});

store.subscribe((counter) => {
  console.log(counter);
});

store.mutation('reset'); // 0

store.action('incrementTwice'); // 2
store.action('incrementTwice'); // 4
store.action('addArray', [5, 8, 10, 15]); // 42

// 'console.log' is called 4 times!

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.

Incredible, 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!

Immutability

By 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!

const source = { life: 42 };
const store = riux(source);

source.life = 1337; // RUNTIME ERROR: Cannot assign to read only property 'life' of object...

const initialState = store.initial();
const currentState = store.current();

initialState.life = 1337; // TS ERROR + RUNTIME ERROR: Cannot assign to 'life' because it is a read-only property.
currentState.life = 1337; // TS ERROR + RUNTIME ERROR: Cannot assign to 'life' because it is a read-only property.

If you wish to avoid the first error you can set the freezeInitialState option to false.

Note that the state retrieved via store.initial() remains unchanged and immutable. Only the source outside the store will not be frozen.

const store = riux(source, { freezeInitialState: false });

source.life = 1337; // NO ERROR!
// ...
initialState.life = 1337; // TS ERROR + RUNTIME ERROR: Cannot assign to 'life' because it is a read-only property.
currentState.life = 1337; // TS ERROR + RUNTIME ERROR: Cannot assign to 'life' because it is a read-only property.

Ok that rocks, one source of truth I like that! But I want more!

Validation with custom parser

By default no data validation is done at runtime, only TypeScript protects you from input type errors at build time or in the IDE.

If 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).

const store = riux(0, {
  parse: (state) => {
    if (typeof state === 'number') return state;
    throw new Error(`expected 'number' got '${typeof state}'`);
  },
  mutations: {
    add(draft, value: number) {
      return draft + value;
    },
  },
});

store.mutation('add', 'prout'); // TS ERROR + RUNTIME ERROR: expected 'number' got 'string'

Another 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?

const store = riux(undefined, {
  parse: (state) => {
    if (state === undefined || typeof state === 'string') return state;
    throw new Error(`expected 'number' got '${typeof state}'`);
  },
  mutations: {
    set(_draft, value: string | undefined) {
      return value;
    },
  },
});

store.current(); // string | undefined

store.mutation('set', undefined);
store.mutation('set', 'or string');
store.mutation('set', 42); // TS ERROR + RUNTIME ERROR: expected 'string' got 'number'

This is starting to be very interesting, but I'm sure we can do better! Right?

Validation with Zod, Superstruct, Yup, tson, ...

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.

const schema = z.object({
  name: z.string().min(3),
  life: z.number(),
});

const store = riux(
  { name: 'nyan', life: 42 },
  {
    parse: (state) => schema.parse(state),
    mutations: {
      setName(draft, name: string) {
        draft.name = name;
      },
      setLife(draft, life: number) {
        draft.life = life;
      },
    },
  },
);

store.current(); // { readonly name: string; readonly life: number }

store.mutation('setName', 'bob'); // { name: 'bob', life: 42 }
store.mutation('setLife', 1337); // { name: 'bob', life: 1337 }

store.mutation('setLife', [true]); // TS ERROR + RUNTIME ZodError: Expected number, received array
store.mutation('setName', 'na'); // TS ERROR + RUNTIME ZodError: String must contain at least 3 character(s)

The Example above is with Zod, but it can work with any library that exposes a function/method with the right signature, like Superstruct, Yup, tson and more...

Need more?

Scaling

As your store grows you will probably want to split your code into several files. Here's how to do it.

// initial-state.ts

export const initialState = {
  name: 'nyan',
  life: 42,
  items: [
    { id: 1, name: 'item-1', rare: true },
    { id: 2, name: 'item-2', rare: false },
    { id: 3, name: 'item-3', rare: false },
  ],
};

export type State = typeof initialState;
export type Item = State['items'][number];
// mutations.ts

import { createMutation, createMutations } from 'riux';
import { initialState, type Item } from './initial-state.js';

// This function could be in another file, for example 'mutations/add-item.ts.
const addItem = createMutation(initialState, (draft, item: Item) => {
  draft.items.push(item);
});

export const mutations = createMutations(initialState, {
  setName: (draft, name: string) => {
    draft.name = name;
  },
  setLife(draft, life: number) {
    draft.life = life;
  },
  addItem,
});
// actions/add-items.ts

import { createAction } from 'riux';
import { initialState, type Item } from '../initial-state.js';
import { mutations } from '../mutations.js';

export const addItems = createAction(initialState, mutations, (mutation, items: Item[]) => {
  for (const item of items) {
    mutation('addItem', item);
  }
});
// actions.ts

import { createAction, createActions } from 'riux';
import { initialState, type Item } from './initial-state.js';
import { mutations } from './mutations.js';
import { addItems } from './actions/add-items.js';

export const actions = createActions(initialState, mutations, {
  setName(mutation, name: string) {
    mutation('setName', name);
  },
  addItems,
});
// store.ts
import { initialState, type Item } from './initial-state.js';
import { mutations } from './mutations.js';
import { actions } from './actions.js';

export const store = createStore(initialState, { mutations, actions });

Type inference

You can extract the TypeScript types of any store with InferState, InferMutations or InferActions.

import riux, { type InferState, type InferMutations } from 'riux';

const store = riux(42, {
  mutations: {
    add: (state, value: number) => state + value,
  },
});

type State = InferState<typeof store>; // number
type Mutations = InferMutations<typeof store>; // { add: (state: number, value: number) => number; }

Ok that's all for now, but if you think something is missing you can open an issue or even better make a pull request.


Scaffolded with @skarab/skaffold

You might also like...

A messaging app built on top of Solana blockchain where you can store and view your messages.

Message App on Solana ๐Ÿ’ฌ This Message application written Rust using Anchor โš“ Setting up the Environment: Rust Installation curl --proto '=https' --tl

Oct 2, 2022

Uncensorable, immutable microblogging platform that is completely decentralized and does not rely on any centralized systems.

Uncensorable, immutable microblogging platform that is completely decentralized and does not rely on any centralized systems.

Zooko Truly decentralized, immutable and uncensorable microblogging Zooko is a working-example, proof-of-concept proving that you can have a decentral

Apr 20, 2022

Serverless for Web3, which is Immutable and Verifiableโœ…

Tender Layer 2 for IPFS / API Renderer for Web3 / Serverless for Ethereum Tender is dynamic content serving P2P Protocol for Ethereum. V1 Design A Cod

Nov 18, 2022

A prototype on how web3 technology can enable us to build an open, immutable, reproducible, and permanently accessible scientific record.

A prototype on how web3 technology can enable us to build an open, immutable, reproducible, and permanently accessible scientific record.

Web3 Research A prototype on how web3 technology can enable us to build an open, immutable, reproducible, and permanently accessible scientific record

Nov 27, 2022

A functional, immutable, type safe and simple dependency injection library inspired by angular.

func-di English | ็ฎ€ไฝ“ไธญๆ–‡ A functional, immutable, type safe and simple dependency injection library inspired by Angular. Why func-di Installation Usage

Dec 11, 2022

Functional Programming with NestJS, Prisma. immutable, pure, stateless

Functional-NestJS Functional Programming with NestJS, Prisma. immutable, pure, stateless. 1. Introduction A production ready typescript backend reposi

Dec 6, 2022

Proof of concept: support immutable trpc servers using lambdas to ensure client/server compatibility

auto-versioned-trpc-aws-lambda Proof of concept to support an automatically versioned AWS Lambda running tRPC to ensure a somewhat graceful and automa

Aug 30, 2022

A library for updating your immutable state in JavaScript applications.

ionic-bond A library for updating immutable states in JavaScript applications. Introduction This library is a very lightweight replacement for immer,

Nov 15, 2022

A zero-dependency, strongly-typed web framework for Bun, Node and Cloudflare workers

nbit A simple, declarative, type-safe way to build web services and REST APIs for Bun, Node and Cloudflare Workers. Examples See some quick examples b

Sep 16, 2022
Releases(v1.1.0)
Owner
In JavaScript We Trust // Mangeur de code
null
Interplanetary Database: A Database built on top of IPFS and made immutable using Ethereum blockchain.

IPDB IPDB (Interplanetary Database) is a key/value store database built on top of IPFS (Interplanetary File System). Project is intended to be an MVP

turinglabs 8 Oct 6, 2022
Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more

Apache MXNet (incubating) for Deep Learning Apache MXNet is a deep learning framework designed for both efficiency and flexibility. It allows you to m

The Apache Software Foundation 20.2k Jan 5, 2023
A fully-typed, low-level, and HyperScript-like Frontend Library ๐Ÿš€

A fully-typed, low-level, and HyperScript-like Frontend Library ??

Eliaz Bobadilla 5 Apr 4, 2022
easily building data structures that are serializable, validatable, and fully typed.

@davecode/structures [beta/wip] Structures is a TypeScript library for easily building data structure classes that are Serializable: Can convert rich

Dave Caruso 2 May 22, 2022
Fully-typed utilities for defining, validating and building your document

zhead Typed utilities for defining, validating and building best-practice document <head>'s. Status: Pre-release Please report any issues ?? Made poss

Harlan Wilton 70 Dec 21, 2022
A helper to use immer as Solid.js Signal to drive state

Solid Immer A helper to use immer as Solid.js Signal to drive state. Installation $ npm install solid-immer Usage Use createImmerSignal to create a im

Shuaiqi Wang 67 Nov 22, 2022
RenderIf is a function that receives a validation as a parameter, and if that validation is true, the content passed as children will be displayed. Try it!

RenderIf RenderIf is a function that receives a validation as a parameter, and if that validation is true, the content passed as children will be disp

Oscar Cornejo Aguila 6 Jul 12, 2022
A lightweight, performant, and simple-to-use wrapper component to stick section headers to the top when scrolling brings them to top

A lightweight, performant, and simple-to-use wrapper component to stick section headers to the top when scrolling brings them to top

Mayank 7 Jun 27, 2022
๐ŸŸข Music player app with a modern homepage, fully-fledged music player, search, lyrics, song exploration features, search, popular music around you, worldwide top charts, and much more.

Music-player-app see the project here. 1. Key Features 2. Technologies I've used Key Features: ?? Fully responsive clean UI. ?? Entirely mobile respo

suraj โœจ 3 Nov 16, 2022
a simple wrapper nestjs dynamic module on top of surrealdb.js driver, with a consumer app to show case library in action, nothing fancy

README README Project Components Dynamic Module Consumer App Install SurrealDb Starts SurrealDb Init surrealDb Database Run App from Source Code Launc

Mรกrio Monteiro 0 Oct 3, 2022