How to Simplify State Management with Valtio
Recommended architecture
UI / React components
↓
useSnapshot(state)
↓
Valtio store
↓
actions / mutations
↓
API / persistence / side effectsA practical project structure:
src/
├── state/
│ ├── store.ts # Root Valtio state
│ ├── actions.ts # State mutations
│ ├── selectors.ts # Derived/read-only state
│ └── types.ts
├── features/
│ ├── auth/
│ ├── projects/
│ └── tasks/
├── components/
└── services/
├── api.ts
└── persistence.tsValtio pattern
import { proxy } from "valtio";export const appState = proxy({
user: null,
projects: [],
ui: {
activeProjectId: null,
isLoading: false,
},
});export const actions = {
setUser(user) {
appState.user = user;
}, addProject(project) {
appState.projects.push(project);
}, setActiveProject(id) {
appState.ui.activeProjectId = id;
},
};
Components should primarily read state:
const snap = useSnapshot(appState);return <div>{snap.projects.length} projects</div>And mutations should go through the action layer rather than being scattered throughout components.
Project workflow
1. Model the state
First identify:
- server state
- client/UI state
- derived state
- transient state
Don’t put everything into Valtio. For example, API-cache/server-state concerns may be better handled by TanStack Query, while Valtio handles application/client state.
2. Create the minimal store
Start with the smallest state model that represents the application’s domain.
3. Centralize mutations
Create actions such as:
createProject()
updateProject()
deleteProject()
selectProject()
setUser()
resetState()This prevents components from becoming state-management layers.
4. Separate side effects
Keep API calls, persistence, analytics, etc. outside the core state model:
Component
↓
Action
↓
Service/API
↓
Action updates Valtio state5. Use snapshots at the UI boundary
React components consume useSnapshot() and should generally avoid knowing how the underlying state is mutated.
6. Add derived state only when necessary
Prefer computed/derived values over duplicating information:
projects
↓
selectedProject
↓
visibleTasksrather than storing all three independently.
7. Test actions independently
The most valuable tests should verify state transitions:
initial state
↓
createProject()
↓
project exists
↓
selectProject()
↓
activeProjectId updatedSimplify State Management with Valtio
You are working on a TypeScript/React application that uses Valtio for client-side state management.
Objective
Refactor the application’s state management so that it is:
- simple and predictable
- centralized without becoming monolithic
- strongly typed
- easy to test
- easy for React components to consume
- resistant to duplicated or inconsistent state
Architecture
Use the following separation of responsibilities:
React Components
↓
useSnapshot()
↓
Valtio State
↓
Actions
↓
Services / APIs / PersistenceState
Keep domain and UI state in Valtio proxies.
Actions
Put state mutations behind explicit action functions.
Examples:
- createProject()
- updateProject()
- deleteProject()
- selectProject()
- setUser()
- resetState()
Actions should contain state-transition logic but should not contain unnecessary UI concerns.
Services
Keep external side effects separate from the state model.
Get REIT monero’s stories in your inbox
Join Medium for free to get updates from this writer.
Examples:
- HTTP/API requests
- localStorage persistence
- analytics
- authentication providers
When an asynchronous operation changes application state, use the service to perform the operation and the action/store layer to apply the resulting state transition.
Rules
- Do not duplicate the same piece of state in multiple places.
- Prefer derived values over storing redundant state.
- Do not introduce a global store for state that is truly local to one component.
- Do not put API-cache management into Valtio if an existing server-state solution already handles it.
- Keep Valtio state strongly typed.
- Keep mutation logic out of presentational components where practical.
- Avoid unnecessary abstractions, wrappers, and generic state-management utilities.
- Preserve existing application behavior while refactoring.
- Make the smallest safe change necessary.
- Do not rewrite unrelated parts of the codebase.
Workflow
Phase 1 — Inspect
Before changing code:
- inspect the existing state-management implementation
- identify all stores and duplicated state
- identify components that directly mutate state
- identify server state versus client state
- identify derived state
- identify existing persistence/API mechanisms
- identify tests covering state behavior
Do not modify code during this phase.
Phase 2 — Design
Propose:
- the Valtio state shape
- action boundaries
- derived-state strategy
- service boundaries
- migration strategy
Explain any important trade-offs briefly.
Wait for confirmation if the proposed refactor is architecturally significant.
Phase 3 — Implement
Implement incrementally.
For each change:
- update types
- update Valtio state
- introduce or update actions
- migrate consumers
- remove obsolete state
- update tests
Avoid a large all-at-once rewrite.
Phase 4 — Verify
After implementation:
- run TypeScript checks
- run linting
- run relevant unit/integration tests
- search for obsolete store usage
- search for duplicated state
- verify that components still render correctly
- verify async/error/loading transitions
Report:
Changed:
- ...Removed:
- ...Tests:
- ...Remaining risks:
- ...
Definition of Done
The refactor is complete when:
- Valtio is the clear source of truth for the intended client state
- mutations are centralized in understandable actions
- React components primarily consume snapshots
- duplicated state has been removed
- server state remains appropriately separated
- TypeScript passes
- tests pass
- no unrelated behavior has changed
- the resulting architecture is simpler than the original one
When uncertain, prefer the simplest implementation that preserves existing behavior.






