Skip to main content

Zustand, Jotai, Valtio

Poimandres is a developer collective and they host various OSS projects. Some of famous projects are react-spring and react-three-fiber. They also provide three popular state managers, namely zustand, jotai, and valtio. This document introduces those three state managers and discuss their features. Three micro-state management libraries from a single GitHub organization may sound counter-intuitive, but they are in different styles.

Zustand

Description

Zustand is a tiny library primarily designed to create module state for React. It's based on an immutable update model, in which state objects can't be modified but always have to be newly created. Render optimization is done manually using selectors. It has a straightforward and yet powerful store creator interface.

Purpose and Use Cases

Zustand primarily designed for module state, which means you define this store in a module and export it.

  • Zustand is very small. So, it's good for a lightweight solution. For example, some apps use react-query for server state, and such apps only need a small global state. Zustand would be a best fit for such small use cases.
  • Immutable state model is the same as what React itself is based on. There would be less mental model switch between React state and zustand global state. So, it would be good for developers who are used to React's useState.
  • Unopinionated. The library has less restrictions, and very open. It's developers responsibility to organize well. It would be best if developers need flexibility with responsibility.

Tradeoffs

Zustand's render optimization with selector functions is also based on immutability – that is, if a selector function returns the same object referentially (or value), it assumes that the object is not changed and avoids re-rendering.

having the same model as React gives us a huge benefit in terms of library simplicity and its small bundle size.

On the other hand, a limitation of Zustand is its manual render optimization with selectors. It requires that we understand object referential equality and the code for selectors tends to require more boilerplate code.

When Should I Consider Using This?

Zustand – or any other libraries with this approach – is a simple addition to the React principle. It's a good recommendation if you need a library with a small bundle size, if you are familiar with referential equality and memoization, or you prefer manual render optimization.

Example

import create from 'zustand';

type Store = {
count: number;
unusedCount: number;
add: (n: number) => void;
};

const useStore = create<Store>((set) => ({
count: 0,
unusedCount: 0,
add: (n) => set((prev) => ({ count: prev.count + n })),
}));

const Counter = () => {
const count = useStore((state) => state.count);
const add = useStore((state) => state.add);
return (
<div>
{count} <button onClick={() => add(3)}>+3</button>
</div>
);
};

export default Counter;

Differences between Zustand and Redux

In some use cases, the developer experience can be similar in Zustand and Redux. Both are based on one-way data flow. In one-way data flow, we dispatch action, which represents a command to update a state, and after the state is updated with action, the new state is propagated to where it's needed.

On the other hand, they differ in how to update states. Redux is based on reducers. While updating states with reducers is a strict method, it leads to more predictability. Zustand takes a flexible approach and it doesn't necessarily use reducers to update states.

Jotai

Description

Jotai is a small library for the global state. It's modeled after useState/useReducer and with what are called atoms, which are usually small pieces of state. Unlike Zustand, it is a component state, and like Zustand, it is an immutable update model.

Purpose and Use Cases

The combination of Context and Subscription is the only way to have a React-oriented global state. If your requirement is Context without extra re-renders, this approach should be your choice.

Tradeoffs

There are two benefits when using Jotai, as follows:

Syntax simplicity

To understand syntax simplicity, let's look at the same counter example with Jotai. First, we need to import some functions from the Jotai library, as follows:

import { atom, useAtom } from 'jotai';

The atom function and the useAtom hook are basic functions provided by Jotai.

An atom represents a piece of a state. An atom is usually a small piece of state, and it is a minimum unit of triggering re-renders. The atom function creates a definition of an atom. The atom function takes one argument to specify an initial value, just as useState does. The following code is used to define a new atom:

const countAtom = atom(0);

Notice the similarity with useState(0). useAtom(countAtom) returns the same tuple, [count, setCount], as useState(0) does.

Non-global state

The second benefit of Jotai is a new capability—that is, non-global state. Atoms can be created and destroyed in the React component lifecycle. This is not possible with the multiple-Context approach, because adding a new state means adding a new Provider component. If you add a new component, all its child components will be remounted, throwing away their states.

Derived atom

The third benefit is derived atom. The atom function provided by library is very primitive, but it's also so flexible that you can combine multiple atoms to implement a functionality. Atoms are building block. By composing atoms based on other atoms, we can implement complicated logic. For example, we can create a new atom that is the sum of two other atoms.

When Should I Consider Using This?

The answer would be to use jotai for component-centric apps.

With the component-centric approach, you would design components first. Some states can be locally defined in components with useState. Other states will be shared across components. For example, in a GUI intensive app, you want to control UI parts in sync, but they are far away in the component tree.

Example

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);
const addCountAtom = atom(null, (_get, set, n: number) => {
set(countAtom, (c) => c + n);
});

const Counter = () => {
const [count] = useAtom(countAtom);
const [, add] = useAtom(addCountAtom);
return (
<div>
{count} <button onClick={() => add(3)}>+3</button>
</div>
);
};

export default Counter;

Differences between Recoil and Jotai

Jotai's API is highly inspired by Recoil. In the beginning, it's intentionally designed to help migration from Recoil to Jotai. The differences are as follows:

  • The biggest difference is the existence of the key string. One of the big motivations of developing Jotai is to omit the key string which let to better DX. Naming is a hard task in coding, especially because the key property has to be unique.

  • Another difference of Jotai is the provider-less mode in Jotai, which allows omission of the Provider component, is technically simple, but very developer-friendly to lower the mental barrier as regards using the library.

Valtio

Description

Valtio is yet another library for global state. Unlike Zustand and Jotai, it's based on the mutating update model. It's primarily for module states like Zustand. It utilizes proxies to get an immutable snapshot, which is required to integrate with React.

Purpose and Use Cases

The API is just JavaScript and everything works behind the scenes. It also leverages proxies to automatically optimize re-renders. It doesn't require a selector to control re-renders. The automatic render optimization is based on a technique called state usage tracking. Using state usage tracking, it can detect which part of the state is used, and it can let a component re-render only if the used part of the state is changed. In the end, developers need to write less code.

Tradeoffs

One big aspect is the mental model. We have two state-updating models. One is for immutable updates and the other for mutable updates. While JavaScript itself allows mutable updates, React is built around immutable states. Hence, if we mix the two models, we should be careful not to confuse ourselves. One possible solution would be to clearly separate the Valtio state and React state so that the mental model switch is reasonable. If it works, Valtio can fit in. Otherwise, maybe stick with immutable updates. The major benefit of mutable updates is we can use native JavaScript functions.

On the other hand, a disadvantage of proxy-based render optimization can be less predictability. Proxies take care of render optimization behind the scenes and sometimes it's hard to debug the behavior. Some may prefer explicit selector-based hooks. In summary, there's no one-size-fits-all solution. It's up to developers to choose the solution that fits their needs.

When Should I Consider Using This?

the answer would be to use valtio for data-centric apps.

The data-centric approach is you have data first regardless of React components. React components are used to represent those data. For example, in game development, it’s likely that you may have game state in advance to design components. You don’t want these data to be controlled by React lifecycle.

Example

import { proxy } from 'valtio';
import { useProxy } from 'valtio/macro';

const state = proxy({
count: 0,
unusedCount: 0,
add: (n: number) => {
state.count += n;
},
});

const Counter = () => {
useProxy(state);
return (
<div>
{state.count} <button onClick={() => state.add(3)}>+3</button>
</div>
);
};

export default Counter;

Differences between Valtio and MobX

Although the motivation is quite different, Valtio is often compared to MobX. Usage-wise, there are some similarities in Valtio and MobX regarding their React binding. Both are based on mutable states and developers can directly mutate state, which results in similar usage. JavaScript is based on mutable objects, so the syntax of mutating an object is very natural and compact. This is a big win for mutable states compared to immutable states.

On the other hand, there is a difference in how they optimize renders. For render optimization, while Valtio uses a hook, MobX React uses a higher-order component (HoC).

Comparing Zustand, Jotai, and Valtio

There is a philosophy that is common in the three libraries: their small API surfaces. All three libraries try their best to provide small API surfaces and let developers compose the APIs as they want.

But then, what are the differences between the three libraries?

There are two aspects:

  • Where does the state reside? In React, there are two approaches. One is the module state, and the other is the component state. A module state is a state that is created at the module level and doesn't belong to React. A component state is a state that is created in React component life cycles and controlled by React. Zustand and Valtio are designed for module states. On the other hand, Jotai is designed for component states. For example, consider Jotai atoms. The following is a definition of countAtom:
const countAtom = atom(0);
  • This countAtom variable holds a config object, and it doesn't hold a value. The atom values are stored in a Provider component. Hence, countAtom can be reused for multiple components. Implementing the same behavior is tricky with module states. With Zustand and Valtio, we would end up using React Context. On the other hand, accessing component states from outside React is technically not possible. We'll likely need some sort of module state to connect to the component states. Whether we use module states or component states depends on the app requirements. Usually, using either module states or component states for global states fulfills the app requirements, but in some rare cases, using both types of states may make sense.
  • What is the state updating style? There is a major difference between Zustand and Valtio. Zustand is based on the immutable state model, while Valtio is based on the mutable state model. The contract in the immutable state model is that objects cannot be changed once created. Suppose you have a state variable such as state = { count: 0 }. If you want to update the count in the immutable state model, you need to create a new object. Hence, incrementing the count by 1 should be state = { count: state.count + 1 }. In the mutable state mode, it could be ++state.count. This is because JavaScript objects are mutable by nature. The benefit of the immutable model is that you can compare the object references to know whether anything has changed. It helps improve performance for large, nested objects. Because React is mostly based on the immutable model, Zustand with the same model has compatibility. Thus, Zustand is a very thin library. On the other hand, Valtio, with the mutable state model, requires filling the gap between the two models. In the end, Zustand and Valtio take different state updating styles. The mutable updating style is very handy, especially when an object is deeply nested.

There are some minor differences among the three libraries, but what's important is the fact that they are based on different principles. If we were to choose one of them, we would need to see which principle fits well with our app requirements and our mental model.

Further Information