Supercharge React: Zustand and Jotai, the Dynamic Duo for Simple, Powerful State Management

React state management evolves with Zustand and Jotai offering simpler alternatives to Redux. They provide lightweight, flexible solutions with minimal boilerplate, excellent TypeScript support, and powerful features for complex state handling in React applications.

Supercharge React: Zustand and Jotai, the Dynamic Duo for Simple, Powerful State Management

React has come a long way since its inception, and state management remains a crucial aspect of building complex applications. While Redux has been the go-to solution for many developers, newer alternatives like Zustand and Jotai have gained popularity for their simplicity and power.

Let’s dive into these lightweight state management libraries and see how they can supercharge your React projects.

Zustand is a tiny state management solution that packs a punch. It’s incredibly easy to set up and use, making it perfect for both small and large-scale applications. One of the things I love about Zustand is its minimal boilerplate. You don’t need to wrap your entire app in a provider or deal with complex reducers.

Here’s a quick example of how you can create a store with Zustand:

import create from 'zustand'

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

Now, you can use this store in any component without any additional setup:

function BearCounter() {
  const bears = useStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}

function Controls() {
  const increasePopulation = useStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

Isn’t that neat? No need for context providers or complex setup. Just create your store and use it wherever you need it.

Zustand also supports middleware out of the box. You can add persistence, devtools, and more with just a few lines of code. For example, to add Redux devtools support:

import { devtools } from 'zustand/middleware'

const useStore = create(devtools((set) => ({
  // your state and actions here
})))

Now you can debug your state changes using the Redux devtools extension in your browser. It’s like having a superpower for debugging!

But what if you need something even more minimal? That’s where Jotai comes in. Jotai takes the atom-based approach to state management, inspired by Recoil but with an even simpler API.

With Jotai, you create atoms for your state, and then use them in your components. Here’s a simple example:

import { atom, useAtom } from 'jotai'

const countAtom = atom(0)

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  )
}

Jotai shines when you need to manage complex, interdependent state. You can create derived atoms that depend on other atoms, making it easy to create computed values:

const doubleCountAtom = atom(
  (get) => get(countAtom) * 2
)

function DoubleCounter() {
  const [doubleCount] = useAtom(doubleCountAtom)
  return <h2>Double count: {doubleCount}</h2>
}

One thing I particularly love about Jotai is how it handles asynchronous state. You can create atoms that resolve promises, making it a breeze to work with API calls or other async operations:

const userAtom = atom(async () => {
  const response = await fetch('https://api.example.com/user')
  return response.json()
})

function User() {
  const [user] = useAtom(userAtom)
  if (user.loading) return <div>Loading...</div>
  if (user.error) return <div>Error: {user.error.message}</div>
  return <div>Welcome, {user.name}!</div>
}

Both Zustand and Jotai offer excellent TypeScript support, which is a huge plus in my book. They provide type inference out of the box, making it easy to catch errors early and improve the overall reliability of your code.

When it comes to performance, both libraries are lightweight and fast. Zustand uses a single store approach, which can be more efficient for larger applications. Jotai, on the other hand, uses fine-grained updates, which can be beneficial for applications with frequent, small updates to the state.

One of the things that drew me to these libraries is their flexibility. They don’t force you into a specific pattern or architecture. You can use them alongside other libraries or even mix them with the built-in React state management tools like useState and useContext.

For example, you might use Zustand for global application state and Jotai for more localized, component-specific state. Or you could use Zustand for your main state management and fall back to useState for simple, component-level state that doesn’t need to be shared.

Another great feature of both libraries is their support for React’s concurrent mode. They’re designed to work seamlessly with React 18’s new features, ensuring your app stays fast and responsive even as it grows in complexity.

Let’s look at a more complex example using Zustand to manage a todo list:

import create from 'zustand'

const useTodoStore = create((set) => ({
  todos: [],
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), text, completed: false }]
  })),
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  })),
  removeTodo: (id) => set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id)
  }))
}))

function TodoList() {
  const todos = useTodoStore(state => state.todos)
  const toggleTodo = useTodoStore(state => state.toggleTodo)
  const removeTodo = useTodoStore(state => state.removeTodo)

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => removeTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  )
}

function AddTodo() {
  const addTodo = useTodoStore(state => state.addTodo)
  const [text, setText] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    if (text.trim()) {
      addTodo(text)
      setText('')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  )
}

This example demonstrates how easy it is to manage complex state with Zustand. We’ve created a store that handles adding, toggling, and removing todos. The components can then access and modify this state without needing to pass props down through multiple levels.

Now, let’s look at a similar example using Jotai:

import { atom, useAtom } from 'jotai'

const todosAtom = atom([])

const addTodoAtom = atom(
  null,
  (get, set, text) => {
    const todos = get(todosAtom)
    set(todosAtom, [...todos, { id: Date.now(), text, completed: false }])
  }
)

const toggleTodoAtom = atom(
  null,
  (get, set, id) => {
    const todos = get(todosAtom)
    set(todosAtom, todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ))
  }
)

const removeTodoAtom = atom(
  null,
  (get, set, id) => {
    const todos = get(todosAtom)
    set(todosAtom, todos.filter(todo => todo.id !== id))
  }
)

function TodoList() {
  const [todos] = useAtom(todosAtom)
  const [, toggleTodo] = useAtom(toggleTodoAtom)
  const [, removeTodo] = useAtom(removeTodoAtom)

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => removeTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  )
}

function AddTodo() {
  const [, addTodo] = useAtom(addTodoAtom)
  const [text, setText] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    if (text.trim()) {
      addTodo(text)
      setText('')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  )
}

In this Jotai example, we’ve created separate atoms for the todos list and each action. This approach allows for even more fine-grained control over our state updates.

Both Zustand and Jotai offer powerful debugging capabilities. With Zustand, you can use the Redux DevTools extension to inspect your state and actions. Jotai provides a debug atom that you can use to log state changes.

One of the things I’ve come to appreciate about these libraries is how they encourage you to think about your state in a more modular way. Instead of having one giant state object, you can break your state down into smaller, more manageable pieces.

This approach not only makes your code more maintainable but also helps with performance. By only updating the specific parts of your state that change, you can avoid unnecessary re-renders and keep your app snappy.

Another cool feature of both libraries is their ability to create custom hooks. This allows you to encapsulate complex state logic and reuse it across your application. For example, with Zustand:

const useCounter = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

function Counter() {
  const { count, increment, decrement } = useCounter()
  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

And with Jotai:

const countAtom = atom(0)
const incrementAtom = atom(null, (get, set) => set(countAtom, get(countAtom) + 1))
const decrementAtom = atom(null, (get, set) => set(countAtom, get(countAtom) - 1))

function useCounter() {
  const [count] = useAtom(countAtom)
  const [, increment] = useAtom(incrementAtom)
  const [, decrement] = useAtom(decrementAtom)
  return { count, increment, decrement }
}

function Counter