Skip to content
Bunshi
GitHub

Molecules

Molecules are the core building block of bunshi. They are functions that return a value.

import { molecule } from "bunshi";

export const RandomMolecule = molecule(() => Math.random());

When this RandomMolecule is used, it will always return the same random number. The value is memoized and cached.

Molecules can depend on other molecules. When molecules depend on other molecules, anything that they depend on will be automatically created.

import { molecule } from "bunshi";

export const RandomMolecule = molecule(() => Math.random());
export const UsernameMolecule = molecule(
  (mol) => `You are user ${mol(RandomMolecule)}`,
);
export const IDMolecule = molecule((mol) => `ID: ${mol(RandomMolecule)}`);

Molecules can also depend on scopes. When a molecule depends on a scope, then an instance will be created for each scope. In other words, your molecule function will be run once per unique scope, instead of once globally for your application.

import { molecule } from "bunshi";
import { userIdScope } from "./scopes";

export const UsernameMolecule = molecule(
  (mol, scope) => `You are user ${scope(userIdScope)}`,
);
export const IDMolecule = molecule((mol) => `ID: ${scope(userIdScope)}`);

use Syntax

New in Bunshi 2.1 is the use syntax for declaring dependencies instead of calling mol of scope.

import { molecule, use } from "bunshi";

export const RandomMolecule = molecule(() => Math.random());
export const UsernameMolecule = molecule(
  () => `You are user ${use(RandomMolecule)}`,
);
export const IDMolecule = molecule(() => `ID: ${use(RandomMolecule)}`);

Molecules can also depend on scopes. When a molecule depends on a scope, then an instance will be created for each scope. In other words, your molecule function will be run once per unique scope, instead of once globally for your application.

import { molecule, use } from "bunshi";
import { userIdScope } from "./scopes";

export const UsernameMolecule = molecule(
  () => `You are user ${use(userIdScope)}`,
);
export const IDMolecule = molecule(() => `ID: ${use(userIdScope)}`);

Rules

  • A molecule without any dependencies or scopes will return a single value.
  • A molecule that depends on scope (i.e. a scoped molecule) will return a single value per unique scope.
  • A molecule that depends on a Scoped Molecule will return a single value per unique scope of it’s dependency.
  • If a molecule calls scope or use with a scope then it will be a scoped molecule
  • If a molecule calls mol or use with a molecule then it will depend on that molecule

Molecule Lifecycle

New in Bunshi 2.1 are the onMount and onUnmount lifecycle hooks. They allow state libraries to be shared and cleaned up at the right time.

onMount

Run some code only when a molecule is mounted.

import { molecule, onMount } from "bunshi";

molecule(() => {
  let i = 0;
  // use `onMount` to guarantee that your code is run only when a molecule is used
  onMount(() => {
    const id = setInterval(() => console.log("Ticking...", i++), 1000);
    // Calls
    return () => clearInterval(id);
  });
  return i;
});

onUnmount

import { molecule, onUnmount } from "bunshi";

molecule(() => {
  onUnmount(() => console.log("Goodbye!"));
  return Math.random();
});

Example

import { useMolecule } from "bunshi/react";
import { useAtom } from "jotai";
import React, { useState } from "react";
import { CountMolecule } from "./molecules";

function Counter() {
  const countAtom = useMolecule(CountMolecule);
  const [count, setCount] = useAtom(countAtom);

  return (
    <div>
      <p>Clicks: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

export default function App() {
  const [isMounted, setMounted] = useState(false);
  return (
    <div>
      Open your browser console to see the logs for lifecycle events.
      <hr />
      <button onClick={() => setMounted((x) => !x)}>
        {isMounted ? "Remove Counter" : "Load Counter"}
      </button>
      {isMounted && (
        <>
          <Counter />
          <Counter />
        </>
      )}
    </div>
  );
}

Lifecycle events are called differently in React Strict Mode.

See:

Best Practices

Molecules don’t just have to return one thing. For example, if you’re using jotai, you would normally return a whole set of atoms (i.e. a molecule).

import { molecule } from "bunshi";
import { atom } from "jotai";

export const FormAtom = molecule((mol, scope) => {
  const dataAtom = atom({});
  const errorsAtom = atom([]);
  const hasErrors = atom((get) => get(errorsAtom).length > 0);

  // Molecules can return a set of atoms
  return {
    dataAtom,
    errorsAtom,
    hasErrors,
  };
});

Molecules don’t just have to return only one type of thing. They can create stores using one or many libraries and wire them all together.

Here’s an example that combines three libraries:

  • Valtio
  • Zustand
  • Jotai

The final derivedAtom is reactive to all 3, and the CountersAtom is useable across Vue, React and vanilla javascript.

import { molecule, ComponentScope } from "bunshi";

// Import Zustand
import create from "zustand/vanilla";

// Import Valtio
import { proxy } from "valtio/vanilla";

// Import Jotai
import { atom } from "jotai";

// Adapters for wiring them together
import { atomWithStore } from "jotai-zustand";
import { atomWithProxy } from "jotai-valtio";

export const CountersAtom = molecule(() => {
  // Zustand store
  const counterStore = create(() => ({ count: 0 }));

  // Valtio proxy
  const counterProxy = proxy({ count: 0 });

  // Jotai atom
  const counterAtom = atom(0);

  // Derived atom that uses all 3 values
  const storeAtom = atomWithStore(counterStore);
  const proxyAtom = atomWithProxy(proxyState);
  const derivedAtom = atom(
    (get) => get(storeAtom) * get(proxyAtom) * get(counterAtom),
  );

  // Molecules returns all the stores
  return {
    counterStore,
    counterProxy,
    counterAtom,
    derivedAtom,
  };
});