xoid
v1.0.0-beta.12
Published
Framework-agnostic state management library designed for simplicity and scalability
Downloads
7,741
Maintainers
Readme
xoid is a framework-agnostic state management library. X in its name is an ode to great projects such as ReduX, MobX and Xstate. It's the result of careful analyses of different state management tools and paradigms. It was designed to be tiny (~1kB gzipped) and easy-to-learn.
The biggest aim of xoid is to unify global state, local component state, finite state machines, and observable streams in the same API. This is especially a big deal for React users where switching between local and global state requires thinking in two different APIs. It might be the very first library to introduce the notion of isomorphic component logic that's able to run across multiple frameworks. With xoid, you can move business logic out of components in a truly framework-agnostic manner.
xoid (zoid is easier to say multiple times) is a robust library based on explicit subscriptions, immutable updates, and a first-class TypeScript support. This makes it ideal for teams. If you prefer implicit subscriptions and mutable updates similar to MobX or Vue 3, you can use @xoid/reactive, a tiny proxy-state layer over *xoid. More features are explained below, and the documentation website.
To install, run the following command:
npm install xoid
Examples
Quick Tutorial
Basic usage of xoid can be learned within a few minutes.
Atom
Atoms are holders of state.
import { atom } from 'xoid'
const $count = atom(3)
console.log($count.value) // 3
$count.set(5)
$count.update((state) => state + 1)
console.log($count.value) // 6
Atoms can have actions.
import { atom } from 'xoid'
const $count = atom(5, (a) => ({
increment: () => a.update(s => s + 1),
decrement: () => a.value-- // `.value` setter is supported too
}))
$count.actions.increment()
There's the .focus
method, which can be used as a selector/lens. xoid is based on immutable updates, so if you "surgically" set state of a focused branch, changes will propagate to the root.
import create from 'xoid'
const $atom = atom({ deeply: { nested: { alpha: 5 } } })
const previousValue = $atom.value
// select `.deeply.nested.alpha`
const $alpha = $atom.focus(s => s.deeply.nested.alpha)
$alpha.set(6)
// root state is replaced with new immutable state
assert($atom.value !== previousValue) // ✅
assert($atom.value.deeply.nested.alpha === 6) // ✅
Derived state
State can be derived from other atoms. This API was heavily inspired by Recoil.
const $alpha = atom(3)
const $beta = atom(5)
// derived atom
const $sum = atom((read) => read($alpha) + read($beta))
Alternatively, .map
method can be used to quickly derive the state from a single atom.
const $alpha = atom(3)
// derived atom
const $doubleAlpha = $alpha.map((s) => s * 2)
Atoms are lazily evaluated. This means that the callback functions of
$sum
and$doubleAlpha
in this example won't execute until the first subscription to these atoms. This is a performance optimization.
Subscriptions
For subscriptions, subscribe
and watch
are used. They are the same, except watch
runs the callback immediately, while subscribe
waits for the first update after subscription.
const unsub = $atom.subscribe((state, previousState) => {
console.log(state, previousState)
})
// later
unsub()
This concludes the basic usage! 🎉
Framework Integrations
Integrating with frameworks is so simple. No configuration, or context providers are needed. Currently all @xoid/react
, @xoid/vue
, and @xoid/svelte
packages export a hook called useAtom
.
React
import { useAtom } from '@xoid/react'
// in a React component
const state = useAtom(atom)
Vue
<script setup>
import { useAtom } from '@xoid/vue'
const value = useAtom(myAtom)
</script>
<template>
<div>{{ value }}</div>
</template>
Svelte
<script>
import { useAtom } from '@xoid/svelte'
let atom = useAtom(myAtom)
</script>
<header>{$atom}</header>
🔥 Isomorphic component logic
This might be the most unique feature of xoid. With xoid, you can write component logic (including lifecycle) ONCE, and run it across multiple frameworks. This feature is for you especially if:
- You're a design system, or a headless UI library maintainer
- You're using multiple frameworks in your project, or refactoring your code from one framework to another
- You dislike React's render cycle and want a simpler, real closure for managing complex state
The following is called a "setup" function:
import { atom, Atom, effect, inject } from 'xoid'
import { ThemeSymbol } from './theme'
export const CounterSetup = ($props: Atom<{ initialValue: number }>) => {
const { initialValue } = $props.value
const $counter = atom(initialValue)
const increment = () => $counter.update((s) => s + 1)
const decrement = () => $counter.update((s) => s - 1)
effect(() => {
console.log('mounted')
return () => console.log('unmounted')
})
const theme = inject(ThemeSymbol)
console.log("theme is obtained using context:", theme)
return { $counter, increment, decrement }
}
All @xoid/react
, @xoid/vue
, and @xoid/svelte
modules have an isomorphic useSetup
function that can consume functions like this.
We're aware that not all users need this feature, so we've built it tree-shakable. If
useAtom
is all you need, you may choose to import it from'@xoid/[FRAMEWORK]/useAtom'
.
With this feature, you can effectively replace the following framework-specific APIs:
| | xoid | React | Vue | Svelte |
|---|---|---|---|---|
| State | create
| useState
/ useReducer
| reactive
/ shallowRef
| readable
/ writable
|
| Derived state | create
| useMemo
| computed
| derived
|
| Lifecycle | effect
| useEffect
| onMounted
, onUnmounted
| onMount
, onDestroy
|
| Dependency injection | inject
| useContext
| inject
| getContext
|
Redux Devtools
Import @xoid/devtools
and set a debugValue
to your atom. It will send values and action names to the Redux Devtools Extension.
import devtools from '@xoid/devtools'
import create from 'xoid'
devtools() // run once
const $atom = atom(
{ alpha: 5 },
($atom) => {
const $alpha = $atom.focus(s => s.alpha)
return {
inc: () => $alpha.update(s => s + 1),
deeply: { nested: { action: () => $alpha.update((s) => s + 1) } }
}
}
)
atom.debugValue = 'myAtom' // enable watching it by the devtools
const { deeply, inc } = atom.actions
inc() // "(myAtom).inc"
deeply.nested.action() // "(myAtom).deeply.nested.action"
atom.focus(s => s.alpha).set(25) // "(myAtom) Update ([timestamp])
Finite state machines
No additional syntax is required for state machines. Just use the create
function.
import { atom } from 'xoid'
import { useAtom } from '@xoid/react'
const createMachine = () => {
const red = { color: '#f00', onClick: () => atom.set(green) }
const green = { color: '#0f0', onClick: () => atom.set(red) }
return atom(red)
}
// in a React component
const { color, onClick } = useAtom(createMachine)
return <div style={{ color }} onClick={onClick} />
If you've read until here, you have enough knowledge to start using xoid. You can refer to the documentation website for more.
Why xoid?
- Easy to learn
- Small bundle size
- Framework-agnostic
- No middlewares needed
- First-class Typescript support
- Easy to work with nested states
- Computed values, transient updates
- Same API to rule them all!
- Global state, Local state, FSMs, Streams
- React, Vue, Svelte, Vanilla JavaScript
Packages
xoid
- Core package@xoid/react
- React integration@xoid/vue
- Vue integration@xoid/svelte
- Svelte integration@xoid/devtools
- Redux Devtools integration@xoid/reactive
- MobX-like proxy state library over xoid
Thanks
Following awesome projects inspired xoid a lot.
Thanks to Anatoly for the pencil&ruler icon #24975.
If you'd like to support the project, consider sponsoring on OpenCollective: