xoid State Management

website·Indexed 32 days ago

https://xoid.dev/

A framework-agnostic state management library designed for simplicity and scalability. xoid uses atoms as its primary primitive and provides an isomorphic API for React, Vue, and Svelte. It features lazy evaluation, derived state, and integrations for Redux DevTools and Immer.

Tokens
10.5K
Snippets
71
Records
71
Agent score
16%

What's inside xoid

  1. Pass reactive props to useSetup in React

    When a second argument is passed to useSetup, it is provided to the callback as a reactive atom. This allows you to reactively track props within the setup closure.
    import { subscribe, use } from 'xoid'
    import { useSetup } from '@xoid/react'
    
    const App = (props: Props) => {
      useSetup(($props) => {
        // `$props` has the type: Atom<Props>
        $props.focus(s => s.something).subscribe(console.log)
      }, props)
      
      return (<div>...</div>)
    }
  2. Install prefill

    Install the prefill utility via npm or pnpm to enable partial application of props for React components.
    npm install prefill
    # or
    pnpm add prefill
  3. Create derived state using atoms and .map()

    Derived atoms compute their value from other atoms and are lazily evaluated, meaning computation only runs upon subscription. You can create a derived atom by passing a function to atom that uses the read utility, or by using the .map() method for simple derivations from a single atom.
    import { atom } from 'xoid'
    
    const $a = atom(3)
    const $b = atom(5)
    
    // Derived atom from multiple sources
    const $sum = atom(read => read($a) + read($b))
    
    // Derived atom from a single source using .map()
    const $double = $a.map(s => s * 2)
  4. Focus on nested state using lenses

    The .focus method allows you to create a selector (lens) for a specific branch of a state tree. Because xoid uses immutable updates, updating a focused atom will automatically propagate the change up to the root atom, replacing the root state with a new immutable version.
    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.update((s) => s + 1)
    
    // Root state is updated immutably
    console.log($atom.value !== previousValue) // true
  5. Manage side effects and dependencies in React with useSetup

    Using useSetup allows React developers to use a closure-based mental model similar to Vue or Svelte. Inside useSetup, you can call effect multiple times or conditionally. All effect calls within a single useSetup block are connected to the same underlying useEffect call, eliminating the need for useCallback or useMemo to persist variables.
    import { useSetup } from '@xoid/react'
    import { effect } from 'xoid'
    
    const App = (props: Props) => {
      useSetup(() => {
        const callback = () => { /* ... */ }
    
        effect(() => {
          window.addEventListener('resize', callback)
          return () => window.removeEventListener('resize', callback)
        })
    
        effect(() => {
          window.addEventListener('orientationchange', callback)
          return () => window.removeEventListener('orientationchange', callback)
        })
      })
      
      return (<div>...</div>)
    }
  6. Derive xoid state from external sources

    You can consume external (non-xoid) state sources—such as a Redux store or an RxJS observable—by passing a getState function and a subscribe function to the get overload within an atom. As long as the external source implements this pair, xoid can derive state from it.
    import store from './reduxStore'
    
    // The atom derives its state from the Redux store's getState and subscribe methods
    const $derivedAtom = atom((get) => get(store.getState, store.subscribe))
  7. Create a custom React hook for state management with Xoid

    To create a custom React hook that manages state and provides actions, define a model using atom that returns an object of actions. Use useAtom within the hook, passing the model factory and setting the second argument to true to ensure the hook returns a 2-item tuple containing the current state and the action object.
    import { atom, useAtom } from 'xoid'
    
    // Define the model with state and actions
    const CounterModel = (value: number) =>
      atom(value, (a) => ({
        increment: () => a.update((s) => s + 1),
        decrement: () => a.update((s) => s - 1),
      }))
    
    // Create the custom hook
    const useCounter = (value: number) => useAtom(() => CounterModel(value), true)
    
    // Usage in a component
    const [state, { increment, decrement }] = useCounter(0)
  8. Lazy evaluation in atoms created with .map()

    Atoms created using the .map method inherit the same lazy evaluation and performance optimizations as standard and derived atoms. The mapping function will not execute until the resulting atom's value is requested, even if the ancestor atom has been updated.
    const $count = atom(() => {
      console.log('Ancestor atom evaluated')
      return 100
    })
    
    const $doubleCount = $count.map((value) => {
      console.log('Evaluation occurred')
      return value * 2
    })
    // nothing's logged on the console yet
    
    $count.update(s => s + 1)
    // Console: "Ancestor atom evaluated"
    
    console.log($doubleCount.value)
    // Console: "Evaluation occurred"
    // Console: 202
  9. Create enhanced atoms to mediate state updates

    An enhanced atom is an atom where the default .set method is replaced with a custom implementation. This allows the atom to act as a mediator, forwarding updates to an external system (like dispatching a Redux action). Because .update uses .set internally, swapping .set also modifies the behavior of .update.
    import store from './reduxStore'
    
    const $mediator = atom((get) => get(store.getState, store.subscribe))
    
    // Swap the default .set method to forward updates to Redux
    $mediator.set = (value: number) => store.dispatch({ type: 'ACTION', payload: value })
    
    // This will now trigger the Redux dispatch via the custom .set implementation
    $mediator.update(s => s + 1)
  10. Create derived state using atoms

    Derived atoms compute their value from other atoms and are lazily evaluated, meaning they only run when subscribed to. For simple derivations from a single atom, the .map() method can be used as a shortcut.
    import { atom } from 'xoid'
    
    const $a = atom(3)
    const $b = atom(5)
    
    // Derived atom using a read function
    const $sum = atom(read => read($a) + read($b))
    
    // Shortcut for deriving from a single atom
    const $double = $a.map(s => s * 2)
  11. Install xoid framework integrations

    xoid provides isomorphic API packages for React, Vue, and Svelte. Install the package corresponding to your framework to enable integration.
    # For React
    npm install @xoid/react
    
    # For Vue
    npm install @xoid/vue
    
    # For Svelte
    npm install @xoid/svelte
  12. Optimize React Context performance using xoid atoms

    To avoid the performance penalty where a Context Provider causes its entire subtree to re-render on every state change, use xoid atoms as the context value. Instead of storing raw state in context, store an atom. This transforms the context into a dependency injection system where the context value reference remains static, and only components that explicitly subscribe to the atom's state via useAtom will re-render.

    Key implementation steps:

    1. Use useSetup to create the atom once, ensuring the context value reference never changes.
    2. Provide the atom via the Context Provider.
    3. Use useContext to retrieve the atom and useAtom to subscribe to its values.
    4. Use .focus() on the atom to create selectors, allowing components to subscribe only to specific sub-properties of the state.
    // App.tsx
    import { atom } from 'xoid'
    import { useSetup } from '@xoid/react'
    import { MyContext } from './MyContext'
    import { ConsumerComponent } from './ConsumerComponent'
    
    export const App = () => {
      // useSetup ensures the atom is created once and the reference remains static
      const contextValue = useSetup(() => atom({ alpha: 3, beta: 5 }))
    
      return (
        <MyContext.Provider value={contextValue}>
          <ConsumerComponent />
        </MyContext.Provider>
      )
    }
    
    // MyComponent.tsx
    import { useContext } from 'react'
    import { useAtom } from '@xoid/react'
    import { MyContext } from './MyContext'
    
    export const MyComponent = () => {
      const $atom = useContext(MyContext)
      // Only this component re-renders when the atom's state changes
      const { alpha, beta } = useAtom($atom)
    
      return (
        <div>
          alpha: {alpha}, beta: {beta}
          <button onClick={() => $atom.focus('alpha').update(s => s + 1)}>
            increment alpha
          </button>
        </div>
      )
    }