xoidlabs / xoid

Framework-agnostic state management library designed for simplicity and scalability âš›
https://xoid.dev
MIT License
158 stars 7 forks source link
framework-agnostic javascript preact react ssr state-machine state-management svelte typescript vanilla vue

React    Vue    Svelte    Vanilla JS    Redux Devtools

Bundle Size Version Downloads License

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

Visit xoid.dev for detailed docs and recipes.


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 { 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) // ✅

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:

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?

Venn diagram that shows that xoid is able to unify global, local state, and finite state machines across React, Vue, Svelte, and vanilla JS

Packages

Thanks

This 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: