Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

🔅 State manager for deeply nested states

License

NotificationsYou must be signed in to change notification settings

Marcisbee/exome

CInpmjsrpackage size

State manager for deeply nested states. Includes integration forReact,Preact,Vue,Svelte,Solid,Lit,Rxjs,Angular &No framework. Can be easily used in microfrontends architecture.

Features

  • 📦Small: Just1 KB minizipped
  • 🚀Fast: Usesno diffing of state changes seebenchmarks
  • 😍Simple: Uses classes as state, methods as actions
  • 🛡Typed: Written in strict TypeScript
  • 🔭Devtools: Redux devtools integration
  • 💨Zero dependencies
// store/counter.tsimport{Exome}from"exome"exportclassCounterextendsExome{publiccount=0publicincrement(){this.count+=1}}exportconstcounter=newCounter()
// components/counter.tsximport{useStore}from"exome/react"import{counter}from"../stores/counter.ts"exportdefaultfunctionApp(){const{ count, increment}=useStore(counter)return(<h1onClick={increment}>{count}</h1>)}

Simple Demo

Table of contents

Installation

To install the stable version:

npm install --save exome

This assumes you are usingnpm as your package manager.

Core concepts

Any piece of state you have, must use a class that extendsExome.

Stores

Store can be a single class or multiple ones. I'd suggest keeping stores small, in terms of property sizes.

State values

Remember that this is quite a regular class (with some behind the scenes logic). So you can write you data inside properties however you'd like. Properties can be public, private, object, arrays, getters, setters, static etc.

Actions

Every method in class is considered as an action. They are only for changing state. Whenever any method is called in Exome it triggers update to middleware and updates view components. Actions can be regular methods or even async ones.

If you want to get something from state via method, use getters.

Usage

Library can be used without typescript, but I mostly recommend using it with typescript as it will guide you through what can and cannot be done as there are no checks without it and can lead to quite nasty bugs.

To create a typed store just create new class with a name of your choosing by extendingExome class exported fromexome library.

import{Exome}from"exome"// We'll have a store called "CounterStore"classCounterStoreextendsExome{// Lets set up one property "count" with default value "0"publiccount=0// Now lets create action that will update "count" valuepublicincrement(){this.count+=1}}

Open in dune.land

That is the basic structure of simple store. It can have as many properties as you'd like. There are no restrictions.

Now we should create an instance ofCounterStore to use it.

constcounter=newCounterStore()

Nice! Now we can start usingcounter state.

Integration

React

UseuseStore() fromexome/react to get store value and re-render component on store change.

import{useStore}from"exome/react"import{counter}from"../stores/counter.ts"exportfunctionExample(){const{ count, increment}=useStore(counter)return<buttononClick={increment}>{count}</button>}

Preact

UseuseStore() fromexome/preact to get store value and re-render component on store change.

import{useStore}from"exome/preact"import{counter}from"../stores/counter.ts"exportfunctionExample(){const{ count, increment}=useStore(counter)return<buttononClick={increment}>{count}</button>}

Vue

UseuseStore() fromexome/vue to get store value and re-render component on store change.

<scriptlang="ts"setup>import{useStore}from"exome/vue";import{counter}from"./store/counter.ts";const{ count, increment}=useStore(counter);</script><template><button@click="increment()">{{ count }}</button></template>

Svelte

UseuseStore() fromexome/svelte to get store value and re-render component on store change.

<script>import{useStore}from"exome/svelte"import{counter}from"./store/counter.js"const{ increment}=counterconstcount=useStore(counter,s=>s.count)</script><main><buttonon:click={increment}>{$count}</button></main>

Solid

UseuseStore() fromexome/solid to get store value and update signal selector on store change.

import{useStore}from"exome/solid"import{counter}from"../stores/counter.ts"exportfunctionExample(){constcount=useStore(counter,s=>s.count)return<buttononClick={counter.increment}>{count}</button>}

Lit

UseStoreController fromexome/lit to get store value and re-render component on store change.

import{StoreController}from"exome/lit"import{counter}from"./store/counter.js"@customElement("counter")classextendsLitElement{privatecounter=newStoreController(this,counter);render(){const{ count, increment}=this.counter.store;returnhtml`<h1@click=${increment}>${count}</h1>    `;}}

Rxjs

UseobservableFromExome fromexome/rxjs to get store value as Observable and trigger it when it changes.

import{observableFromExome}from"exome/rxjs"import{counter}from"./store/counter.js"observableFromExome(countStore).pipe(map(({ count})=>count),distinctUntilChanged()).subscribe((value)=>{console.log("Count changed to",value);});setInterval(counter.increment,1000);

Angular

signals (>=16)

UseuseStore fromexome/angular to get store value and update signal selector on store change.

import{useStore}from"exome/angular"import{counter}from"./store/counter.ts"@Component({selector:'my-app',template:`    <h1 (click)="increment()">      {{count}}    </h1>  `,})exportclassApp{publiccount=useStore(counter,(s)=>s.count);publicincrement(){counter.increment();}}

observables (<=15)

Angular support is handled via rxjs async pipes!

UseobservableFromExome fromexome/rxjs to get store value as Observable and trigger it when it changes.

import{observableFromExome}from"exome/rxjs"import{counter}from"./store/counter.ts"@Component({selector:'my-app',template:`    <h1 *ngIf="(counter$ | async) as counter" (click)="counter.increment()">      {{counter.count}}    </h1>  `,})exportclassApp{publiccounter$=observableFromExome(counter)}

No framework

Usesubscribe fromexome to get store value in subscription callback event when it changes.

import{subscribe}from"exome"import{counter}from"./store/counter.js"constunsubscribe=subscribe(counter,({ count})=>{console.log("Count changed to",count)})setInterval(counter.increment,1000)setTimeout(unsubscribe,5000)

Redux devtools

You can use redux devtools extension to explore Exome store chunk by chunk.

Just addexomeReduxDevtools middleware viaaddMiddleware function exported by library before you start defining store.

import{addMiddleware}from'exome'import{exomeReduxDevtools}from'exome/devtools'addMiddleware(exomeReduxDevtools({name:'Exome Playground'}))

It all will look something like this:

Exome using Redux Devtools

API

Exome

A class with underlying logic that handles state changes. Every store must be extended from this class.

abstractclassExome{}

useStore

Is function exported from "exome/react".

functionuseStore<TextendsExome>(store:T):Readonly<T>

Arguments

  1. store(Exome): State to watch changes from. Without Exome being passed in this function, react component will not be updated when particular Exome updates.

Returns

  • Exome: Same store is returned.

Example

import{useStore}from"exome/react"constcounter=newCounter()functionApp(){const{ count, increment}=useStore(counter)return<buttononClick={increment}>{count}</button>}

Open in dune.land

onAction

Function that calls callback whenever specific action on Exome is called.

functiononAction(store:typeofExome):Unsubscribe

Arguments

  1. store(Exome constructor): Store that has desired action to listen to.
  2. action(string): method (action) name on store instance.
  3. callback(Function): Callback that will be triggered before or after action.
    Arguments
    • instance(Exome): Instance where action is taking place.
    • action(String): Action name.
    • payload(any[]): Array of arguments passed in action.
  4. type("before" | "after"): when to run callback - before or after action, default is"after".

Returns

  • Function: Unsubscribes this action listener

Example

import{onAction}from"exome"constunsubscribe=onAction(Person,'rename',(instance,action,payload)=>{console.log(`Person${instance} was renamed to${payload[0]}`);// Unsubscribe is no longer neededunsubscribe();},'before')

saveState

Function that saves snapshot of current state for any Exome and returns string.

functionsaveState(store:Exome):string

Arguments

  1. store(Exome): State to save state from (will save full state tree with nested Exomes).

Returns

  • String: Stringified Exome instance

Example

import{saveState}from"exome/state"constsaved=saveState(counter)

loadState

Function that loads saved state in any Exome instance.

functionloadState(store:Exome,state:string):Record<string,any>

Arguments

  1. store(Exome): Store to load saved state to.
  2. state(String): Saved state string fromsaveState output.

Returns

  • Object: Data that is loaded into state, but without Exome instance (if for any reason you have to have this data).

Example

import{loadState,registerLoadable}from"exome/state"registerLoadable({  Counter})constnewCounter=newCounter()constloaded=loadState(newCounter,saved)loaded.count// e.g. = 15loaded.increment// undefinednewCounter.count// new counter instance has all of the state applied so also = 15newCounter.increment// [Function]

registerLoadable

Function that registers Exomes that can be loaded from saved state vialoadState.

functionregisterLoadable(config:Record<string,typeofExome>,):void

Arguments

  1. config(Object): Saved state string fromsaveState output.
    • key(String): Name of the Exome state class (e.g."Counter").
    • value(Exome constructor): Class of named Exome (e.g.Counter).

Returns

  • void

Example

import{loadState,registerLoadable}from"exome/state"registerLoadable({  Counter,  SampleStore})

addMiddleware

Function that adds middleware to Exome. It takes in callback that will be called every time before an action is called.

React hook integration is actually a middleware.

typeMiddleware=(instance:Exome,action:string,payload:any[])=>(void|Function)functionaddMiddleware(fn:Middleware):void

Arguments

  1. callback(Function): Callback that will be triggeredBEFORE action is started.
    Arguments

    • instance(Exome): Instance where action is taking place.
    • action(String): Action name.
    • payload(any[]): Array of arguments passed in action.

    Returns

    • (void | Function): Callback can return function that will be calledAFTER action is completed.

Returns

  • void: Nothingness...

Example

import{Exome,addMiddleware}from"exome"addMiddleware((instance,name,payload)=>{if(!(instanceinstanceofTimer)){return;}console.log(`before action "${name}"`,instance.time);return()=>{console.log(`after action "${name}"`,instance.time);};});classTimerextendsExome{publictime=0;publicincrement(){this.time+=1;}}consttimer=newTimer()setInterval(timer.increment,1000)// > before action "increment", 0// > after action "increment", 1//   ... after 1s// > before action "increment", 1// > after action "increment", 2//   ...

Open in Codesandbox

FAQ

Q: Can I use Exome inside Exome?

YES! It was designed for that exact purpose.Exome can have deeply nested Exomes inside itself. And whenever new Exome is used in child component, it has to be wrapped inuseStore hook and that's the only rule.

For example:

classTodoextendsExome{constructor(publicmessage:string,publiccompleted=false){super();}publictoggle(){this.completed=!this.completed;}}classStoreextendsExome{constructor(publiclist:Todo[]){super();}}conststore=newStore([newTodo("Code a new state library",true),newTodo("Write documentation")]);functionTodoView({ todo}:{todo:Todo}){const{ message, completed, toggle}=useStore(todo);return(<li><strongstyle={{textDecoration:completed ?"line-through" :"initial"}}>{message}</strong>      &nbsp;<buttononClick={toggle}>toggle</button></li>);}functionApp(){const{ list}=useStore(store);return(<ul>{list.map((todo)=>(<TodoViewkey={getExomeId(todo)}todo={todo}/>))}</ul>);}

Open in dune.land

Q: Can deep state structure be saved to string and then loaded back as an instance?

YES! This was also one of key requirements for this. We can save full state from any Exome withsaveState, save it to file or database and the load that string up onto Exome instance withloadState.

For example:

constsavedState=saveState(store)constnewStore=newStore()loadState(newStore,savedState)

Q: Can I update state outside of React component?

Absolutely. You can even share store across multiple React instances (or if we're looking into future - across multiple frameworks).

For example:

classTimerextendsExome{publictime=0publicincrement(){this.time+=1}}consttimer=newTimer()setInterval(timer.increment,1000)functionApp(){const{ time}=useStore(timer)return<h1>{time}</h1>}

Open in Codesandbox

IE support

To run Exome on IE, you must haveSymbol andPromise polyfills and down-transpile to ES5 as usual. And that's it!

Motivation

I stumbled upon a need to store deeply nested store and manage chunks of them individually and regular flux selector/action architecture just didn't make much sense anymore. So I started to prototype what would ideal deeply nested store interaction look like and I saw that we could simply use classes for this.

Goals I set for this project:

  • Easy usage with deeply nested state chunks (array in array)
  • Type safe with TypeScript
  • To have actions be only way of editing state
  • To have effects trigger extra actions
  • Redux devtool support

License

MIT ©Marcis Bergmanis

About

🔅 State manager for deeply nested states

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Contributors3

  •  
  •  
  •  

[8]ページ先頭

©2009-2025 Movatter.jp