A State Management Tour: Proxy State With Valtio

Guillaume Billey
Guillaume BilleyJune 23, 2022
#architecture#js#react

Most React applications start small, with clearly separated states and components. As an application gets more complex, it becomes more and more difficult to organize and maintain its state: some components have to share some state with others, some have to get a subset of the state, and some have to re-render when a specific part of the state changes... How do I deal with this complexity?

And so started my journey into state management tools.

During my search for the truth, I stumbled upon Valtio, a proxy state management for React. This is the library I'll explore in this post. I will explain how it works, the concepts behind it, and why it can be helpful. Several other posts will follow, exploring other state management solutions.

A Quick Refresher On Proxies

Valtio says of itself that it "makes proxy-state simple". But, what is "proxy state"? I have a vague knowledge of the Proxy Pattern, is it related?

Indeed, it is based on this concept. To make it short, let's say I have a book object. A proxy "wraps" the book object. To access the object, I have to pass through the proxy, and so, the proxy can do extra operations before returning the book, like logging.

Proxies were introduced in ES6.

Let's try to do logging with JavaScript's Proxy. If I want to log all access to an object, I can use a Proxy to intercept get calls, and do a console.log with the parameters. To do so, I use the Proxy class, with 2 parameters: target (the object to wrap) and handler (the behaviors I want to add).

// the handler intercepts the read action (get) and calls console.log before returning the value.
const loggingHandler = {
    get(target, property) {
        console.log(`Reading ${property}`);
        return target[property];
    },
};

const target = {
    count: 1,
    title: 'A random object',
};

// bind the proxy to a target
const proxyTarget = new Proxy(target, loggingHandler);

Now, when I read a property of proxyTarget, it runs the loggingHandler at the same time:

> const count = proxyTarget.count;
Reading count
> console.log(proxyTarget.title)
Reading title
'A random object'

Proxies seem cool, but, why would I use them in a React app?

Sharing State With Props

One of the most common problems when dealing with multiple components is to manage shared state, and to update the concerned components whenever a state property changes. How do I handle that?

The first solution I think about is to use props. I can pass the state as props to the components I want to share the state with.

const Store = () => {
  // Declare state
  const [beers] = useState([
    { name: "Hazy rider", brewery: "Hoppy Road" },
    { name: "BP Porter", brewery: "Bon Poison" }
  ]);
  return (
    <>
      {/* Pass the state as props */}
      <Basket beers={beers} />
    </>
  );
};

// Get the state from the props
const Basket = ({ beers }) => {
  return (
    <>
      {beers.map((beer) => (
        <div key={beer.name}> {`${beer.name} - ${beer.brewery}`}</div>
      ))}
    </>
  );
};

But it can quickly become a burden to pass props down several layers (a.k.a. "Prop Drilling"). And what if the props I want to pass are not for a child component but for a parent one, or a sibling? It's not the ideal solution.

Sharing State With Context

React simplifies this with Context. It allows giving access to the state to all the components inside a Context.Provider.

// Create the context
const BeersContext = createContext();

const Store = () => {
  const state = {
    beers: [
      {
        name: "Hazy rider",
        brewery: "Hoppy Road"
      },
      {
        name: "BP Porter",
        brewery: "Bon Poison"
      }
    ]
  };
  // Wrap it in contextProvider
  return (
    <BeersContext.Provider value={state}>
      <Basket />
    </BeersContext.Provider>
  );
};

const Basket = () => {
  // Get it from context
  const { beers } = useContext(BeersContext);
  return (
    <>
      {beers.map((beer) => (
        <div key={beer.name}> {`${beer.name} - ${beer.brewery}`}</div>
      ))}
    </>
  );
};

It seems to be better: no more prop drilling. But it has some limitations: components that want access to the context need to be wrapped in a Provider. Furthermore, I can't subscribe to a subpart of a context. As a consequence, all the components that only depend on a subpart of the context re-renders whenever any state change occurs, even if that change doesn't concern them. This can quickly cause performance issues.

Using Proxies To Share State

How can I solve this problem?

When reading a state object, I want to subscribe to updates of parts of this state. Ideally, I'd like to read state properties without any special syntax - just like when reading properties of a regular object. When the property I subscribed to changes, I'd like the component to re-render.

Proxies make that possible: by wrapping state in a proxy, and using a pub/sub system instead of React context, I can get fine-grained state updates with an intuitive syntax. Components basically subscribe to the state to get notified of changes.

Wrapping state

The proxy redefines the mutations (set, deleteProperty) to notify listeners whenever a change occurs.

Notify listeners

And, that's it...

Building A Proxy-State With Valtio

Valtio provides a set of methods to ease the creation of a proxified store.

I create a proxy state using the proxy function:

import { proxy } from 'valtio';

export const state = proxy({
    count: 1,
    title: 'A dumb state',
});

I can also define mutations that update the proxy state. For instance:

export const setTitle = newTitle => {
    state.title = newTitle;
};

Note that when I do state.title = newTitle, I manipulate a proxy of the state, not the state itself. Thus by setting a value, behind the scene it triggers some other behaviors defined by Valtio.

Reading And Updating A Proxy-State

Now I can use this state in React components. But if I read directly the state object, I miss the rendering optimization done by Valtio. So I am going to use the useSnapshot hook. It returns another proxy, that subscribes to the source proxy, and therefore benefits from the optimization. The rule of thumb is: read from snapshots in the render function, otherwise use the source proxy.

import { useSnapshot } from 'valtio';
import { state, setTitle } from './state';

const DumbComponent = () => {
    const snapshot = useSnapshot(state);
    return (
        <input
            type="text"
            // I read from snapshot
            value={snapshot.title}
            // but I update via the source
            onChange={evt => setTitle(evt.target.value)}
        />
    );
};

With this technique, I can manage almost all the state management use cases. But Valtio provides some utilities that help a little more.

Computing Derived State

Sometimes I need to derive state from other pieces of state. For example, if I have a basket of products (let's say... beers), I may want to handle the number of products in the basket.

I can do so by using the derive utility. derive take as a parameter an object of functions defining the derivation we want. Those functions are passed a get function to access the state.

import { derive, proxy } from 'valtio';

export const state = proxy({
    selectedBeers: [
        { name: 'Guinness', quantity: 2 },
        { name: 'Budweiser', quantity: 3 },
    ],
});

export const countState = derive({
    total: get =>
        // I access the state with the get function provided by Valtio
        get(state).selectedBeers.reduce((acc, beer) => {
          acc += beer.quantity;
          return acc;
        }, 0),
});

In a React component, I can use the derived value with useSnapshot, just like for the proxy state:

import { countState } from './basket';

const TotalComponent = () => {
    const { total } = useSnapshot(countState);

    return <div>{total}</div>;
};

That's it for the principle of derived state. To dive deeper, let's take a (little) more complex case.

Fine-Tuned Rendering With Valtio

I've written a state representing a basket for a beer store:

import { proxy, useSnapshot } from 'valtio';
import { derive } from 'valtio/utils';
import { Beer } from '../types';

interface BasketState {
    isBasketOpen: boolean;
    selectedBeers: { beer: Beer, quantity: number }[];
}

export const basketState = proxy<BasketState>({
  isBasketOpen: false,
  selectedBeers: [],
});

I also wrote a component called <BeerCard>, which displays info about a specific beer, together with the number of bottles of this beer that are in the basket.

I could write this component like this:

[...]
const BeerCard = (props: { beer: Beer }) => {
  const { beer } = props;
  const { selectedBeers } = useSnapshot(basketState);
  const quantity = useMemo(
    () => selectedBeers.find((b) => b.beer.id === beer.id)?.quantity,
    [selectedBeers, beer]
  );
  return (
    <Card
      key={beer.id}
      sx={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        flexDirection: "column",
      }}
    >
    [...]
        <Typography color="text.primary" gutterBottom>
          {beer.name} - {quantity}
        </Typography>
    [...]
    </Card>
  );
};

But this approach is not optimized, as every BeerCard re-renders when updating the basket.

Valtio unoptimize

I want React to re-render only the card containing the beer updated by the user. With Valtio, I can use the derive method.

First, let's add a method in my store to get a derived state for a specific beer:

const derivedState: Record<string,  { quantity: number }> = {};

export const getBeerQuantityState = (beerId: number) => {
    // If I haven't yet derived a state for the beer
    if (!derivedState[beerId]) {
        // I derive it
        const derivedBeerState = derive({
            quantity: get =>
                get(basketState).selectedBeers.find(
                    (b: { beer: Beer, quantity: number }) =>
                        b.beer.id === beerId,
                )?.quantity || 0,
        });
        // Store it
        derivedState[beerId] = derivedBeerState;
    }
    // And return the derived state of the beer
    return derivedState[beerId];
};

I end up with a derivedState containing something like :

Object
  id1: Proxy {quantity: 2} // a derived state for beer with id 'id1'
  id2: Proxy {quantity: 4} // a derived state for beer with id 'id2'
  id3: Proxy {quantity: 1} // a derived state for beer with id 'id3'
  id4: Proxy {quantity: 0} // ...

And then in my BeerCard Component :

const { quantity } =
    // I get the derived state for the beer
    useSnapshot < { quantity: number } > getBeerQuantityState(beer.id);

And... that's all. I have a proxy state for each BeerCard, and Valtio can do its magic tricks to notify only the right component.

[...]
const BeerCard = (props: { beer: Beer }) => {
  const { beer } = props;
  const { selectedBeers } = useSnapshot(basketState);
- const quantity = useMemo(
-   () => selectedBeers.find((b) => b.beer.id === beer.id)?.quantity,
-   [selectedBeers, beer]
- );
+ const { quantity } =
+   useSnapshot < { quantity: number } > getBeerQuantityState(beer.id);
  return (
    <Card
      key={beer.id}
      sx={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        flexDirection: "column",
      }}
    >
    [...]
        <Typography color="text.primary" gutterBottom>
          {beer.name} - {quantity}
        </Typography>
    [...]
    </Card>
  );
};

Valtio optimize

Behind The Magic Of Valtio

Valtio looks a bit magic, so I wanted to understand how it works. Let's take a look inside to demystify it.

First, as stated before, it uses a Proxy, and it adds behaviors on the set method of the state.

Let's have a quick look inside the handler defined in Valtio code (I've truncated and modified a little bit the code to ease the understanding):

set(target: T, prop: string | symbol, value: any, receiver: any) {
      const prevValue = Reflect.get(target, prop, receiver)
      if (this.is(prevValue, value)) {
        return true
      }
      [...]
      nextValue = value
      [...]
      Reflect.set(target, prop, nextValue, receiver)
      notifyUpdate(['set', [prop], value, prevValue])
      return true
    },
  }
Complete source code - if you want
  set(target: T, prop: string | symbol, value: any, receiver: any) {
        const prevValue = Reflect.get(target, prop, receiver)
        if (this.is(prevValue, value)) {
          return true
        }
        const childListeners = prevValue?.[LISTENERS]
        if (childListeners) {
          childListeners.delete(popPropListener(prop))
        }
        if (isObject(value)) {
          value = getUntracked(value) || value
        }
        let nextValue: any
        if (Object.getOwnPropertyDescriptor(target, prop)?.set) {
          nextValue = value
        } else if (value instanceof Promise) {
          nextValue = value
            .then((v) => {
              nextValue[PROMISE_RESULT] = v
              notifyUpdate(['resolve', [prop], v])
              return v
            })
            .catch((e) => {
              nextValue[PROMISE_ERROR] = e
              notifyUpdate(['reject', [prop], e])
            })
        } else if (value?.[LISTENERS]) {
          nextValue = value
          nextValue[LISTENERS].add(getPropListener(prop))
        } else if (this.canProxy(value)) {
          nextValue = proxy(value)
          nextValue[LISTENERS].add(getPropListener(prop))
        } else {
          nextValue = value
        }
        Reflect.set(target, prop, nextValue, receiver)
        notifyUpdate(['set', [prop], value, prevValue])
        return true
      }

This method starts by getting the previous value Reflect.get(target, prop, receiver). Reflect is a JS object that allows intercepting some methods. Here, it intercepts the get method. Then it compares it to the new value. If there is no change, it does nothing. Otherwise, it sets the value Reflect.set(target, prop, nextValue, receiver), and notifies all listeners notifyUpdate(['set', [prop], value, prevValue]).

The useSnapshot hook adds listeners to the proxy:

export function useSnapshot<T extends object>(
  proxyObject: T,
  options?: Options
): Snapshot<T> {
  [...]
    useCallback(
      (callback) => {
        lastCallback.current = callback
        const unsub = subscribe(proxyObject, callback, notifyInSync)
        return () => {
          unsub()
          lastCallback.current = undefined
        }
      },
      [proxyObject, notifyInSync]
    )
  [...]
Complete source code - if you want
export function useSnapshot<T extends object>(
  proxyObject: T,
  options?: Options
): Snapshot<T> {
  const affected = new WeakMap()
  const lastAffected = useRef<typeof affected>()
  const lastCallback = useRef<() => void>()
  const notifyInSync = options?.sync
  const currSnapshot = useSyncExternalStore(
    useCallback(
      (callback) => {
        lastCallback.current = callback
        const unsub = subscribe(proxyObject, callback, notifyInSync)
        return () => {
          unsub()
          lastCallback.current = undefined
        }
      },
      [proxyObject, notifyInSync]
    ),
    useMemo(() => {
      let prevSnapshot: Snapshot<T> | undefined
      return () => {
        const nextSnapshot = snapshot(proxyObject)
        try {
          if (
            prevSnapshot &&
            lastAffected.current &&
            !isChanged(
              prevSnapshot,
              nextSnapshot,
              lastAffected.current,
              new WeakMap()
            )
          ) {
            // not changed
            return prevSnapshot
          }
        } catch (e) {
          // ignore if a promise or something is thrown
        }
        return (prevSnapshot = nextSnapshot)
      }
    }, [proxyObject]),
    useCallback(() => snapshot(proxyObject), [proxyObject])
  )
  const currVersion = getVersion(proxyObject)
  useEffect(() => {
    lastAffected.current = affected
    // check if state has changed between render and commit
    if (currVersion !== getVersion(proxyObject)) {
      if (lastCallback.current) {
        lastCallback.current()
      } else if (
        typeof process === 'object' &&
        process.env.NODE_ENV !== 'production'
      ) {
        console.warn('[Bug] last callback is undefined')
      }
    }
  })
  if (__DEV__) {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useAffectedDebugValue(currSnapshot, affected)
  }
  const proxyCache = useMemo(() => new WeakMap(), []) // per-hook proxyCache
  return createProxyToCompare(currSnapshot, affected, proxyCache)
}

The important part is the subscribe(proxyObject, callback, notifyInSync), which subscribes a listener to the proxy.

So I have a proxy that creates a Proxy wrapping state, and that notifies a list of listeners when there are updates. I also have a useSnapshot, that creates another Proxy subscribing to the original proxyObject via a callback.

That explains the fundamental mechanism behind Valtio. In addition to that, Valtio also does some optimization, but I will not dive into this.

Will I use Valtio ?

I'll definitely use Valtio on my next projects! Its simplicity and conciseness are really good points in my opinion. It makes me wonder why I've used more verbose and complex patterns such as Redux in the past (I know, I know, some use cases can justify such a choice (do they? :P)).

So if you want to try a new state management library, Valtio would be my advice. Just keep in mind that it lets you organize your state as you want. You are responsible for that, so choose wisely how you split your state.

You can find the code of my experiment with Valtio in the guilbill/beer-store-valtio repository on GitHub.

Did you like this article? Share it!