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.