State manager for deeply nested states
npm install exomeState manager for deeply nested states. Includes integration for React, Preact, Vue, Svelte, Solid, Lit, Rxjs, Angular & No framework. Can be easily used in microfrontends architecture.
- 📦 Small: Just 1 KB minizipped
- 🚀 Fast: Uses no diffing of state changes see benchmarks
- 😍 Simple: Uses classes as state, methods as actions
- 🛡 Typed: Written in strict TypeScript
- 🔭 Devtools: Redux devtools integration
- 💨 Zero dependencies
``ts
// store/counter.ts
import { Exome } from "exome"
export class Counter extends Exome {
public count = 0
public increment() {
this.count += 1
}
}
export const counter = new Counter()
`
`tsx
// components/counter.tsx
import { useStore } from "exome/react"
import { counter } from "../stores/counter.ts"
export default function App() {
const { count, increment } = useStore(counter)
return (
Table of contents
- Core concepts
- Usage
- Integration
- React
- Preact
- Vue
- Svelte
- Solid
- Lit
- Rxjs
- Angular
- No framework
- Redux devtools
- API
- FAQ
- Benchmarks
- Motivation
Installation
To install the stable version:
`bash
npm install --save exome
`
This assumes you are using npm as your package manager.Core concepts
Any piece of state you have, must use a class that extends Exome.StoresStore can be a single class or multiple ones. I'd suggest keeping stores small, in terms of property sizes.
State valuesRemember 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.
ActionsEvery 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 extending
Exome class exported from exome library.`ts
import { Exome } from "exome"// We'll have a store called "CounterStore"
class CounterStore extends Exome {
// Lets set up one property "count" with default value "0"
public count = 0
// Now lets create action that will update "count" value
public increment() {
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 of
CounterStore to use it.`ts
const counter = new CounterStore()
`Nice! Now we can start using
counter state.Integration
React
Use useStore() from exome/react to get store value and re-render component on store change.`tsx
import { useStore } from "exome/react"
import { counter } from "../stores/counter.ts"export function Example() {
const { count, increment } = useStore(counter)
return
}
`Preact
Use useStore() from exome/preact to get store value and re-render component on store change.`tsx
import { useStore } from "exome/preact"
import { counter } from "../stores/counter.ts"export function Example() {
const { count, increment } = useStore(counter)
return
}
`Vue
Use useStore() from exome/vue to get store value and re-render component on store change.`html
`Svelte
Use useStore() from exome/svelte to get store value and re-render component on store change.`html
`Solid
Use useStore() from exome/solid to get store value and update signal selector on store change.`tsx
import { useStore } from "exome/solid"
import { counter } from "../stores/counter.ts"export function Example() {
const count = useStore(counter, s => s.count)
return
}
`Lit
Use StoreController from exome/lit to get store value and re-render component on store change.`ts
import { StoreController } from "exome/lit"
import { counter } from "./store/counter.js"@customElement("counter")
class extends LitElement {
private counter = new StoreController(this, counter);
render() {
const { count, increment } = this.counter.store;
return html
;
}
}
`Rxjs
Use observableFromExome from exome/rxjs to get store value as Observable and trigger it when it changes.`ts
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
$3
Use useStore from exome/angular to get store value and update signal selector on store change.`ts
import { useStore } from "exome/angular"
import { counter } from "./store/counter.ts"@Component({
selector: 'my-app',
template:
,
})
export class App {
public count = useStore(counter, (s) => s.count);
public increment() {
counter.increment();
}
}
`$3
Angular support is handled via rxjs async pipes!Use
observableFromExome from exome/rxjs to get store value as Observable and trigger it when it changes.`ts
import { observableFromExome } from "exome/rxjs"
import { counter } from "./store/counter.ts"@Component({
selector: 'my-app',
template:
,
})
export class App {
public counter$ = observableFromExome(counter)
}
`No framework
Use subscribe from exome to get store value in subscription callback event when it changes.`ts
import { subscribe } from "exome"
import { counter } from "./store/counter.js"const unsubscribe = 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 add
exomeReduxDevtools middleware via addMiddleware function exported by library before you start defining store.`ts
import { addMiddleware } from 'exome'
import { exomeReduxDevtools } from 'exome/devtools'addMiddleware(
exomeReduxDevtools({
name: 'Exome Playground'
})
)
`It all will look something like this:
API
$3
A class with underlying logic that handles state changes. Every store must be extended from this class.`ts
abstract class Exome {}
`$3
Is function exported from "exome/react".`ts
function useStore(store: T): Readonly
`__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__
`tsx
import { useStore } from "exome/react"const counter = new Counter()
function App() {
const { count, increment } = useStore(counter)
return
}
`
__Open in dune.land__$3
Function that calls callback whenever specific action on Exome is called.`ts
function onAction(store: typeof Exome): 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__
`tsx
import { onAction } from "exome"const unsubscribe = onAction(
Person,
'rename',
(instance, action, payload) => {
console.log(
Person ${instance} was renamed to ${payload[0]}); // Unsubscribe is no longer needed
unsubscribe();
},
'before'
)
`$3
Function that saves snapshot of current state for any Exome and returns string.`ts
function saveState(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__
`tsx
import { saveState } from "exome/state"const saved = saveState(counter)
`$3
Function that loads saved state in any Exome instance.`ts
function loadState(
store: Exome,
state: string
): Record
`__Arguments__
1.
store _(Exome)_: Store to load saved state to.
2. state _(String)_: Saved state string from saveState output.__Returns__
- _Object_: Data that is loaded into state, but without Exome instance (if for any reason you have to have this data).
__Example__
`ts
import { loadState, registerLoadable } from "exome/state"registerLoadable({
Counter
})
const newCounter = new Counter()
const loaded = loadState(newCounter, saved)
loaded.count // e.g. = 15
loaded.increment // undefined
newCounter.count // new counter instance has all of the state applied so also = 15
newCounter.increment // [Function]
`$3
Function that registers Exomes that can be loaded from saved state via loadState.`ts
function registerLoadable(
config: Record,
): void
`__Arguments__
1.
config _(Object)_: Saved state string from saveState 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__
`ts
import { loadState, registerLoadable } from "exome/state"registerLoadable({
Counter,
SampleStore
})
`$3
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.
`ts
type Middleware = (instance: Exome, action: string, payload: any[]) => (void | Function)function addMiddleware(fn: Middleware): void
`__Arguments__
1.
callback _(Function)_: Callback that will be triggered BEFORE 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 called
AFTER action is completed.__Returns__
- _void_: Nothingness...
__Example__
`ts
import { Exome, addMiddleware } from "exome"addMiddleware((instance, name, payload) => {
if (!(instance instanceof Timer)) {
return;
}
console.log(
before action "${name}", instance.time); return () => {
console.log(
after action "${name}", instance.time);
};
});class Timer extends Exome {
public time = 0;
public increment() {
this.time += 1;
}
}
const timer = new Timer()
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
$3
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 in useStore hook and that's the only rule.For example:
`tsx
class Todo extends Exome {
constructor(public message: string, public completed = false) {
super();
} public toggle() {
this.completed = !this.completed;
}
}
class Store extends Exome {
constructor(public list: Todo[]) {
super();
}
}
const store = new Store([
new Todo("Code a new state library", true),
new Todo("Write documentation")
]);
function TodoView({ todo }: { todo: Todo }) {
const { message, completed, toggle } = useStore(todo);
return (
style={{
textDecoration: completed ? "line-through" : "initial"
}}
>
{message}
);
}function App() {
const { list } = useStore(store);
return (
{list.map((todo) => (
))}
);
}
`
__Open in dune.land__$3
YES! This was also one of key requirements for this. We can save full state from any Exome with saveState, save it to file or database and the load that string up onto Exome instance with loadState.For example:
`tsx
const savedState = saveState(store)const newStore = new Store()
loadState(newStore, savedState)
`$3
Absolutely. You can even share store across multiple React instances (or if we're looking into future - across multiple frameworks).For example:
`tsx
class Timer extends Exome {
public time = 0 public increment() {
this.time += 1
}
}
const timer = new Timer()
setInterval(timer.increment, 1000)
function App() {
const { time } = useStore(timer)
return
{time}
}
`
__Open in Codesandbox__IE support
To run Exome on IE, you must have Symbol and Promise` polyfills and down-transpile to ES5 as usual. And that's it!Goals I set for this project:
- [x] Easy usage with deeply nested state chunks (array in array)
- [x] Type safe with TypeScript
- [x] To have actions be only way of editing state
- [x] To have effects trigger extra actions
- [x] Redux devtool support