How to: persist a valtio state

Prabhakar Bhat
3 min readMay 22, 2021

--

Why valtio?

JavaScript ecosystem has quite a few choices for state management. Redux, Mobx, VueX, Recoil, Hookstate, Valtio… the list seems endless. Valtio is a relatively new way of managing state in this list, and it uses JavaScript proxies to provide reactivity.

A typical redux based setup has the following:

  • One global store
  • Multiple action creators
  • Many reducer functions
  • Thunks/sagas to handle async operations
  • You may also use reselect to create computed values
  • Working with immutable data may need additional care

This setup quickly gets verbose and the single global store grows quite large.

In contrast, you can have the following setup in valtio:

  • Multiple state objects based on your app’s features
  • Actions are plain functions which directly modify the state. No reducers required. Since these can be async functions, you don’t necessarily need additional abstractions to handle async operations

Thus, developing with valtio is a lot simpler than redux, and that’s why you should consider valtio.

Local persistence

Redux has a popular solution for this. redux-persist library is used to store the state to localStorage on web or AsyncStorage on react native. As of now, no such library exists for valtio. However, it’s fairly straightforward to implement yourself with valtio subscriptions.

Subscriptions allow you to access the state from anywhere, and do something when state changes. This is a good place to persist the state to local storage.

Let’s begin by creating a valtio state.

import { proxy } from 'valtio';

const state = proxy({ todos: [] });

Next, let’s subscribe to the state changes, and save to localStorage.

subscribe(state, () => {
localStorage.set('todosState', JSON.stringify(state));
});

The state is now persisted to local storage any time the state changes, but we are not hydrating the state anywhere. It will be a blank list again if the user refreshes/starts a new session.

Simplest way to solve this is to parse the stored data and provide it as initial value to the state.

const storedStateString = localStorage.getItem('todosState');
const initialState = storedStateString ? JSON.parse(storedStateString) : { todos: [] };

The final solution:

// states/todos.js
import { proxy } from 'valtio';
const storedStateString = localStorage.getItem('todosState');
const initialState = storedStateString ? JSON.parse(storedStateString) : { todos: [] };
const state = proxy(initialState);subscribe(state, () => {
localStorage.set('todosState', JSON.stringify(state));
});
export default state;

React Native & AsyncStorage

While the solution above is great for web, it doesn’t work for React Native where you need to use AsyncStorage. The hydration will have to happen asynchronously, and thus you need to defer the usage of the state until it is hydrated.

// states/todos.js
import AsyncStorage from '@react-native-async-storage/async-storage';
import { proxy } from 'valtio';

const state = proxy({ todos: [], hydrated: false });
subscribe(state, () => {
AsyncStorage.setItem('todosState', JSON.stringify(state));
});
export const hydrateState = async () => {
const storedStateString = await AsyncStorage.getItem('todosState');
if(storedStateString){
state.todos = JSON.parse(storedStateString).todos;
}
state.hydrated = true;
}
export default state;// App.tsx
import { useSnapshot } from 'valtio';
import React, { useEffect } from 'react';
import state, { hydrateState } from './states/todos';
const App = () => {
const { hydrated, todos } = useSnapshot(state);
useEffect(() => {
hydrateState();
}, []);
if(hydrated){
// Return your actual component which renders the app
}
// return a loading screen
}

Closing thoughts

While local persistence is great, you need take care of data migrations and cache invalidations. In any significantly complex app, you will have multiple states which you need to organise, persist, and hydrate. Expect more posts on these topics soon!

--

--

Prabhakar Bhat
Prabhakar Bhat

Responses (1)