Using Valtio for Component State

While Valtio is excellent for global state, it can also be used to manage complex component-level state. This is particularly useful when you need to share state between nested components without prop drilling or when you want the benefits of Valtio's reactivity for a state that is tied to a component's lifecycle.

To achieve this, you can create a proxy instance inside a useRef to ensure the state persists across re-renders without being recreated.

Using useRef and React.Context

A common pattern is to create the proxy state within a component and provide it to its children via React Context.

import { createContext, useContext, useRef } from 'react';
import { proxy, useSnapshot } from 'valtio';

// 1. Create a context
const MyContext = createContext();

// 2. Create a provider component that initializes the state
const MyProvider = ({ children }) => {
  // Create the proxy state inside a useRef to persist it
  const state = useRef(proxy({ count: 0 })).current;
  return <MyContext.Provider value={state}>{children}</MyContext.Provider>;
};

// 3. Create a consumer component
const MyCounter = () => {
  // Get the state from the context
  const state = useContext(MyContext);
  const snap = useSnapshot(state);

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

// 4. Wrap your components with the provider
function App() {
  return (
    <MyProvider>
      <MyCounter />
    </MyProvider>
  );
}

This pattern confines the Valtio state to the component subtree wrapped by MyProvider, making MyCounter and any other consumers reusable and self-contained.

Alternatives to useRef

If you prefer not to use useRef directly, several community libraries offer elegant alternatives:

  • use-constant: A simple hook to create a constant value for the lifetime of a component.
  • bunshi: A library for creating scoped, injectable atoms that works well with Valtio.

Bunshi Example

Bunshi provides a clean way to define and inject state without manually managing context.

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

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

// Component.tsx
import { useMolecule } from 'bunshi/react';
import { useSnapshot } from 'valtio';
import { CounterState } from './molecules';

function Counter() {
  const state = useMolecule(CounterState);
  const snap = useSnapshot(state);
  // ...
}

CodeSandbox Example

See a live example of using Valtio for component-level state with useRef and Context.

https://codesandbox.io/s/valtio-component-ye5tbg?file=/src/App.tsx