Skip to content
Bunshi
GitHub

Motivation

In jotai, it is easy to do global state, but jotai is much more powerful when used for more than just global state!

The problem is the atom lifecycle, because we need to follow the mantras of jotai:

The challenge with jotai is getting a reference to an atom outside of a component/hook. It is hard to do recursive atoms or scoped atoms. Jotai molecules fixes this:

  • You can lift state up, by changing your molecule definitions
  • When you lift state up, or push state down, you don’t need to refactor your component

Before Bunshi

Let’s examine this idea by looking at an example Counter component.

The most important function in these examples is the createAtom function, it creates all the state:

const createAtom = () => atom(0);

Global state

Here is an example of the two synchronized Counter components using global state.

import { atom, useAtom } from "jotai";

const createAtom = () => atom(0);
const countAtom = createAtom();

const Counter = () => {
  const [count, setCount] = useAtom(countAtom);
  return (
    <div>
      count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </div>
  );
};

export const App = () => (
  <>
    <Counter /> <Counter />
  </>
);

Component state

Here is the same component with Component State. Notice the use of useMemo:

import { atom, useAtom } from "jotai";

const createAtom = () => atom(0);

const Counter = () => {
  const countAtom = useMemo(createAtom, []);
  const [count, setCount] = useAtom(countAtom);
  return (
    <div>
      count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </div>
  );
};

export const App = () => (
  <>
    <Counter /> <Counter />
  </>
);

Scoped state

Here is a component with context-based state:

import { atom, useAtom } from "jotai";

const createAtom = () => atom(0);

const CountAtomContext = React.createContext(createAtom());
const useCountAtom = () => useContext(CountAtomContext);
const CountAtomScopeProvider = ({ children }) => {
  const countAtom = useMemo(createAtom, []);
  return (
    <CountAtomContext.Provider value={countAtom}>
      {children}
    </CountAtomContext.Provider>
  );
};

const Counter = () => {
  const countAtom = useCountAtom();
  const [count, setCount] = useAtom(countAtom);
  return (
    <div>
      count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </div>
  );
};

export const App = () => (
  <CountAtomScopeProvider>
    <Counter />
    <CountAtomScopeProvider>
      <Counter />
      <Counter />
    </CountAtomScopeProvider>
  </CountAtomScopeProvider>
);

Or, to make that context scoped based off a scoped context

import { atom, useAtom } from "jotai";

const createAtom = (userId: string) =>
  atom(userId === "bob@example.com" ? 0 : 1);

const CountAtomContext = React.createContext(createAtom());
const useCountAtom = () => useContext(CountAtomContext);
const CountAtomScopeProvider = ({ children, userId }) => {
  // Create a new atom for every user Id
  const countAtom = useMemo(() => createAtom(userId), [userId]);
  return (
    <CountAtomContext.Provider value={countAtom}>
      {children}
    </CountAtomContext.Provider>
  );
};

const Counter = () => {
  const countAtom = useCountAtom();
  const [count, setCount] = useAtom(countAtom);
  return (
    <div>
      count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </div>
  );
};

export const App = () => (
  <CountAtomScopeProvider userId="bob@example.com">
    <Counter />
    <Counter />
    <CountAtomScopeProvider userId="tom@example.com">
      <Counter />
      <Counter />
    </CountAtomScopeProvider>
  </CountAtomScopeProvider>
);

Summary

For all of these examples;

  • to lift state up, or push state down, we had to refactor <Counter>
  • the more specific we want the scope of our state, the more boilerplate is required

Using Bunshi

With molecules, you can change how atoms are created without having to refactor your components.

Global State

Here is an example of the <Counter> component with global state:

import { atom, useAtom } from "jotai";
import { molecule, useMolecule } from "bunshi/react";

const countMolecule = molecule(() => atom(0));

const Counter = () => {
  const countAtom = useMolecule(countMolecule);
  const [count, setCount] = useAtom(countAtom);
  return (
    <div>
      count: {count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </div>
  );
};

export const App = () => <Counter />;

Component state

To have unique state per component, change the molecule definition to use ComponentScope and don’t refactor the component. Here is an example of the <Counter> component with component state:

import { atom, useAtom } from "jotai";
import { molecule, useMolecule, ComponentScope } from "bunshi/react";

const countMolecule = molecule((mol, scope) => {
  scope(ComponentScope);
  console.log("Creating a new atom for component");
  return atom(0);
});

// ... Counter unchanged

export const App = () => <Counter />;

Scoped state

For a scoped molecule, change the molecule definition and don’t refactor the component. Here is an example of the <Counter> component with scoped context state:

import { atom, useAtom } from "jotai";
import {
  molecule,
  useMolecule,
  createScope,
  ScopeProvider,
} from "bunshi/react";

const UserScope = createScope(undefined);
const countMolecule = molecule((mol, scope) => {
  const userId = scope(UserScope);
  console.log("Creating a new atom for", userId);
  return atom(0);
});

// ... Counter unchanged

export const App = () => (
  <ScopeProvider scope={UserScope} value={"bob@example.com"}>
    <Counter />
  </ScopeProvider>
);

Summary

Now, you can follow the React best practice of Lifting State Up by adding a molecule, and then lifting the state up, or pushing the state down.

You can also choose to use the favorite state management library and use it for individual components, sections of your application, or your entire application.