React Vue Svelte Vanilla JS Redux Devtools
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
Basic usage of xoid can be learned within a few minutes.
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 { atom } 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) // ✅
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.
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! 🎉
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
.
import { useAtom } from '@xoid/react'
// in a React component
const state = useAtom(atom)
<script setup>
import { useAtom } from '@xoid/vue'
const value = useAtom(myAtom)
</script>
<template>
<div>{{ value }}</div>
</template>
<script>
import { useAtom } from '@xoid/svelte'
let atom = useAtom(myAtom)
</script>
<header>{$atom}</header>
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:
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 |
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])
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.
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 xoidThis repo initially started as a fork of zustand. Due to this, GitHub's "Contributors" section can be misleading. Majority of the people on that list are actually Zustand's contributors until September 2020.
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: