Bunshi

repository·main·Indexed 43 days ago

https://github.com/saasquatch/bunshi

A tiny, fast, and dependency-free dependency injection and state management library (v2.2.0). It allows developers to create state stores and manage dependencies by lifting state up or pushing state down across React, Vue, and vanilla JS environments. It integrates with state management solutions such as jotai, valtio, zustand, nanostores, and vue reactive.

Tokens
20.8K
Snippets
67
Records
107
Agent score
54%

What's inside Bunshi

  1. Overview of Bunshi

    main

    Bunshi is a lightweight (3kb), dependency-free library designed for creating state stores and managing dependencies. It enables developers to lift state up or push state down across different environments.

    Bunshi is framework-agnostic and works with:

    • React
    • Vue
    • Vanilla Javascript & Typescript

    It is designed to integrate seamlessly with existing state management solutions, including:

    • jotai atoms
    • valtio proxies
    • zustand stores
    • nanostores
    • vue reactive
  2. What is Bunshi?

    main

    Bunshi (formerly jotai-molecules) is a tiny (1.18kb minified/gzipped), dependency-free library designed for creating state stores and managing dependencies. It acts as a general-purpose dependency injection tool that allows you to 'lift state up' or 'push state down'.

    It is compatible with various state management libraries, including:

    • Jotai atoms
    • Valtio proxies
    • Zustand stores
    • Nanostores
    • Vue refs

    Key technical characteristics:

    • Memory Efficient: Uses WeakMap to prevent memory leaks.
    • Performance: Uses memoization to prevent unnecessary re-renders.
    • Framework Agnostic: Works with any frontend or backend JavaScript framework, with official integrations for React and Vue.
  3. Decouple Vue state logic from UI components

    main

    When using Bunshi with Vue, you can follow these best practices to maintain a clean architecture:

    1. Use Vue's reactivity API (ref, reactive) for the actual state storage.
    2. Use Bunshi to manage the lifecycle and scope of those reactive primitives.
    3. Decouple logic: Define your state and business logic in Bunshi molecules rather than inside .vue components.
    4. Scale easily: Move state from component-level to global-level by simply changing how the molecule is scoped in Bunshi, without needing to modify the component's internal logic.
  4. Use hierarchical injectors to manage interface bindings

    main

    Bunshi supports a parent-child injector hierarchy. Child injectors inherit all bindings from their parent, but can also define their own bindings or override existing parent bindings.

    • Inheritance: A child injector can access any interface bound in its parent.
    • Overriding: If a child injector provides a binding for an interface already defined in the parent, the child's implementation takes precedence for that injector and its descendants.
    const parentInjector = createInjector({
      bindings: [[ApiClientInterface, ProductionApiClientMolecule]]
    });
    
    const childInjector = createInjector({
      parent: parentInjector,
      bindings: [[CacheInterface, MemoryCacheMolecule]]
    });
    
    // childInjector can resolve both ApiClientInterface (from parent) and CacheInterface (local)
    const parentInjector = createInjector({
      bindings: [[ApiClientInterface, ProductionApiClientMolecule]]
    });
    
    const childInjector = createInjector({
      parent: parentInjector,
      bindings: [[CacheInterface, MemoryCacheMolecule]]
    });
  5. Scope a Molecule with ComponentScope

    main

    By default, molecules are shared globally. To provide a unique instance of a molecule for every component instance, use the use(ComponentScope) pattern inside the molecule definition. This allows you to push or pull state without changing the component's implementation.

    import { ComponentScope, molecule, use } from "bunshi";
    import { atom } from "jotai/vanilla";
    
    export const CountMolecule = molecule(() => {
      // Because scope is used here, this molecule is component scoped
      use(ComponentScope);
      return atom(0);
    });
  6. Benefits of using Bunshi with Zag

    main

    Using Bunshi alongside Zag provides several architectural advantages:

    • Lifecycle Management: Use Zag for global state stores that are started and stopped at exactly the right time.
    • Zero-Touch Refactoring: Move state from component-scoped to global (or vice versa) without needing to touch the component code itself.
    • Framework Agnostic: Use the vanilla JavaScript API of Zag to avoid framework lock-in, keeping state logic decoupled from the UI framework.
    • Scalable Sharing: Easily share machines across different pages, components, or sections of your application.
  7. Combine global and component-scoped Jotai atoms with Bunshi

    main

    Bunshi enables you to use Jotai atoms for both local component state and global application state within the same logic. This allows you to create derived values that depend on both scopes simultaneously—a pattern that is difficult to achieve with standard Jotai Provider or useStore implementations alone.

    In a typical Bunshi setup:

    • Global state: Atoms defined in a global scope shared across the app.
    • Component state: Atoms scoped to a specific component instance.
    • Derived state: Atoms that combine values from both global and local scopes.
    // Example concept: A multiplier combining global and local state
    // molecules.ts
    import { atom } from 'jotai';
    
    // Global atom
    export const globalMultiplierAtom = atom(2);
    
    // Local atom (scoped via Bunshi)
    export const localCountAtom = atom(0);
    
    // Derived atom combining both
    export const combinedAtom = atom((get) => {
      const multiplier = get(globalMultiplierAtom);
      const count = get(localCountAtom);
      return count * multiplier;
    });
  8. How scopes work for form state management

    main

    Scopes are ideal for managing state that should survive for the lifecycle of a specific component, such as a form. By creating a scope for every form and making molecules depend on that scope, a new instance of the molecules is created for each unique scope instance.

    Workflow:

    1. Create a scope object: Use createScope to define the scope.
    2. Depend on the scope in molecules: Inside a molecule definition, call scope(yourScope) to make the molecule scoped. This ensures the state (e.g., a nanostore or Jotai atom) is unique to that scope.
    3. Provide the scope: Use provideScope at the top level of your form component tree.
    4. Consume the molecule: Use useMolecule in child components to access the scoped state.
    import { createScope } from "bunshi";
    
    // 1. Create scope
    export const formScope = createScope<unknown>(undefined);
    
    import { molecule } from "bunshi";
    import { atom } from "nanostores";
    
    // 2. Depend on scope in a molecule
    export const FormErrors = molecule((mol, scope) => {
      // Calling `scope` makes `FormErrors` a *scoped* molecule
      scope(formScope);
    
      const errors = atom<string[]>([]);
    
      function addError(error: string) {
        errors.set([...errors.get(), error]);
      }
    
      return {
        addError,
        errors,
      };
    });
  9. What are Molecules in Bunshi

    main

    Molecules are the core building blocks of Bunshi. They are functions that return a value. When a molecule is used, its value is memoized and cached, meaning it will always return the same value for subsequent calls within the same context.

    Key characteristics:

    • Memoization: Values are cached after the first execution.
    • Dependency Injection: Molecules can depend on other molecules or scopes.
    • Scoping: Molecules can be global (single value for the app) or scoped (one instance per unique scope).
    import { molecule } from "bunshi";
    
    export const RandomMolecule = molecule(() => Math.random());
  10. Why use Bunshi with Nanostores?

    main

    While Nanostores can be used with simple global variables for state that is always global and never lazy, Bunshi provides several advantages for managing Nanostores:

    • Scoping: Bunshi helps you scope atoms, allowing you to easily "pull state up" (move from component to global) or "push state down" (move from global to component) the component tree.
    • Component-level state: It enables using Nanostores for state that should be local to a component instance.
    • Framework Agnostic Logic: By sticking to the vanilla JavaScript API for Nanostores and using Bunshi for orchestration, you decouple your state logic from specific UI frameworks (React, Vue, etc.).
    • Refactor-friendly: You can move state from the component level to the global level without needing to refactor your UI components.