How to Use Valtio with React Context

Valtio is often used for global state, but it's also an excellent choice for state that is local to a specific component tree. By combining Valtio with React Context, you can create encapsulated, reusable state modules that only live for the duration of a component's lifecycle.

The Basic Pattern

The pattern involves creating a Valtio proxy inside a useRef to ensure its persistence across re-renders, and then providing this proxy to child components via a Context Provider.

Here's a step-by-step guide:

1. Create a React Context

import { createContext } from 'react';

const MyContext = createContext();

2. Create a Provider Component

This component will create and hold the Valtio state. Using useRef is crucial to prevent the proxy from being re-created on every render.

import { useRef } from 'react';
import { proxy } from 'valtio';

const MyProvider = ({ children }) => {
  const state = useRef(proxy({ count: 0 })).current;
  return <MyContext.Provider value={state}>{children}</MyContext.Provider>;
};

3. Create a Consumer Component or Hook

Child components can now access the state using useContext.

import { useContext } from 'react';
import { useSnapshot } from 'valtio';

const MyCounter = () => {
  const state = useContext(MyContext);
  const snap = useSnapshot(state);

  return (
    <>
      Count: {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </>
  );
};

4. Use the Provider in Your App

Wrap the part of your component tree that needs access to this state with your provider.

function App() {
  return (
    <MyProvider>
      <MyCounter />
      {/* Other components inside MyProvider can also access the state */}
    </MyProvider>
  );
}

Alternatives

If you find the useRef pattern cumbersome, consider these alternatives:

  • use-constant: A simple hook for creating a value that is constant for the lifetime of a component.
  • bunshi: A library for creating scoped, injectable state "atoms" that integrates well with Valtio.

Bunshi Example

Bunshi simplifies the process of creating and providing scoped state.

// molecules.ts
import { molecule } from 'bunshi';
import { proxy } from 'valtio';

export const CounterStateMolecule = molecule(() => proxy({ count: 0 }));

// MyCounter.tsx
import { useMolecule } from 'bunshi/react';
import { useSnapshot } from 'valtio';
import { CounterStateMolecule } from './molecules';

function MyCounter() {
  const state = useMolecule(CounterStateMolecule);
  const snap = useSnapshot(state);
  // ... use state and snap
}

This approach provides the same benefits as Context but with less boilerplate.