07. Use Case Scenario 1: Zustand

Re-rendering optimization with zustand, handling structured data

🔖 1. What is Zustand?

Key Features

  • A lightweight state management library that allows simple and intuitive global state management in React applications

  • You can define and use global state with just one or two hooks and simple function calls without complex setup

  • When creating a store, you simply define state and actions that change state in object form

  • It subscribes only to necessary state and re-renders only when that state changes, reducing unnecessary rendering

  • Asynchronous logic (API communication, data fetching, etc.) can be easily included in state definition logic, making it convenient to handle side effects

Example Code

Adapted from code on Zustand official documentation main page

import create from 'zustand';

// 1. Define global state (store)
// Create a store (useStore) by defining state and actions (increase, decrease, reset) with create function
const useStore = create((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
  decrease: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

// When count value is updated through set, the state change function
// only components that depend on count re-render

function Counter() {
  // 2. Use state
  // When useStore hook is called within a component, you get count and each function
  const { count, increase, decrease, reset } = useStore();

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increase}>A bear is born!!</button>
      <button onClick={decrease}>A bear dies..</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

export default function App() {
  return (
    <div>
      <h2>Zustand Bear 🐻</h2>
      <Counter />
    </div>
  );
}

Should I Use Zustand?

  • As such, Zustand defines a store and gets necessary state and functions through hooks

  • Since global state is defined and managed at once, state change logic can be gathered in one place, so maintenance is expected to be easy even as scale grows

  • Therefore, if you want to clearly structure global state at once and simplify state management flow through one store, Zustand would be good

  • Jotai requires breaking down state into atom units from the beginning, so I'm not sure if it can be used well in the early stages of a project

🔖 2. Immutable State Model

Explaining 'Immutable State Model'

  • When managing state, Zustand updates state by creating a new object with only the changed parts instead of modifying the original state object

  • In other words, when state changes, instead of modifying the existing object, it creates a new object and puts that object in place of the existing state

  • And unchanged parts (e.g., other fields or internal objects) reuse the previous ones as they are, reducing unnecessary rendering

  • To use an analogy, instead of erasing content written on paper with an eraser and writing again, you write only the changed content on new paper and replace it. Unchanged content can reuse previous content without wasting resources

Example Code

import create from 'zustand';

const useStore = create((set) => ({
  user: { name: 'Ella', age: 20 },
  // State change function (maintaining immutability)
  updateUserName: (newName) =>
    set((state) => ({
      // Don't modify user object directly, return new object
      // Only name changes, age doesn't change, so reuse existing user.age
      user: { ...state.user, name: newName },
    })),

  items: ['apple', 'banana'],
  addItem: (item) =>
    set((state) => ({
      // Don't modify existing array, create new array and return
      items: [...state.items, item],
    })),
}));

function App() {
  const { user, updateUserName, items, addItem } = useStore();

  return (
    <div>
      <p>
        User: {user.name}, {user.age}
      </p>
      <button onClick={() => updateUserName('Ella')}>
        Change name to Chloe
      </button>

      <p>Items: {items.join(', ')}</p>
      <button onClick={() => addItem('orange')}>Give me Orange too</button>
    </div>
  );
}

export default App;

Feeling the Benefits Through Examples

  • As such, Zustand makes it easy to understand how state has changed

  • By utilizing the immutability pattern that returns new objects, the process of comparing previous state and new state becomes simple

  • When the book says "Since you only need to check the equality of references to state objects to know if there's a change, you don't need to check the entire value of objects," it means you can determine whether state has changed just by checking "Is it the same object or a different object?"

🔖 3. Manual Rendering Optimization

What is Selector-Based Re-rendering Control?

  • It's a strategy to subscribe only to specific parts of state that components need and re-render components only when those parts actually change

  • This way, even if other parts of global state change, components are not affected, reducing unnecessary re-rendering

  • Components that use only a very small part of large global state will only render when that small part changes, which will be advantageous for performance optimization

Example Code

import create from 'zustand';

const useStore = create((set) => ({
  user: { name: 'Ella', age: 20 },
  items: ['apple', 'banana'],
  setName: (name) =>
    set((state) => ({
      user: { ...state.user, name },
    })),
  addItem: (item) =>
    set((state) => ({
      items: [...state.items, item],
    })),
}));

// Selector: get only user's name
const selectUserName = (state) => state.user.name;

// Selector: get only the length of items array
const selectItemsCount = (state) => state.items.length;

function UserNameDisplay() {
  // Subscribe only to user.name value, don't re-render if name doesn't change
  const name = useStore(selectUserName);
  console.log('UserNameDisplay rendered');
  return <div>User Name: {name}</div>;
}

function ItemsCountDisplay() {
  // Subscribe only to items.length value, don't re-render if items count doesn't change
  const count = useStore(selectItemsCount);
  console.log('ItemsCountDisplay rendered');
  return <div>Items Count: {count}</div>;
}

function App() {
  const setName = useStore((state) => state.setName);
  const addItem = useStore((state) => state.addItem);

  return (
    <div>
      <UserNameDisplay />
      <ItemsCountDisplay />
      <button onClick={() => setName('Bob')}>Change name to Chloe</button>
      <button onClick={() => addItem('orange')}>Give me Orange too</button>
    </div>
  );
}

export default App;

My Thoughts

This chapter provides a practical introduction to Zustand, showing how it simplifies global state management compared to more complex solutions like Redux. The key insight is that Zustand provides a good balance between simplicity and functionality.

The discussion about immutable state models is particularly important - it shows how Zustand leverages React's optimization patterns to provide better performance without requiring complex setup.

The selector-based optimization examples demonstrate how Zustand can be used efficiently in real applications, showing that you can get good performance without over-engineering.

Code Examples

// ✅ GOOD: Basic Zustand store setup
interface BearState {
  bears: number;
  fish: number;
  increasePopulation: () => void;
  removeAllBears: () => void;
  addFish: (amount: number) => void;
}

const useBearStore = create<BearState>((set) => ({
  bears: 0,
  fish: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
  addFish: (amount) => set((state) => ({ fish: state.fish + amount })),
}));

// ✅ GOOD: Using selectors for performance optimization
function BearCounter() {
  const bears = useBearStore((state) => state.bears); // Only re-renders when bears change
  const increasePopulation = useBearStore((state) => state.increasePopulation);

  return (
    <div>
      <h1>{bears} bears around here...</h1>
      <button onClick={increasePopulation}>Add a bear</button>
    </div>
  );
}

function FishCounter() {
  const fish = useBearStore((state) => state.fish); // Only re-renders when fish change
  const addFish = useBearStore((state) => state.addFish);

  return (
    <div>
      <h1>{fish} fish in the sea</h1>
      <button onClick={() => addFish(5)}>Add 5 fish</button>
    </div>
  );
}

// ✅ GOOD: Complex state with nested objects
interface UserState {
  user: {
    name: string;
    age: number;
    preferences: {
      theme: 'light' | 'dark';
      language: string;
    };
  };
  updateName: (name: string) => void;
  updateAge: (age: number) => void;
  updateTheme: (theme: 'light' | 'dark') => void;
  updateLanguage: (language: string) => void;
}

const useUserStore = create<UserState>((set) => ({
  user: {
    name: 'Ella',
    age: 25,
    preferences: {
      theme: 'light',
      language: 'en',
    },
  },
  updateName: (name) =>
    set((state) => ({
      user: { ...state.user, name },
    })),
  updateAge: (age) =>
    set((state) => ({
      user: { ...state.user, age },
    })),
  updateTheme: (theme) =>
    set((state) => ({
      user: {
        ...state.user,
        preferences: { ...state.user.preferences, theme },
      },
    })),
  updateLanguage: (language) =>
    set((state) => ({
      user: {
        ...state.user,
        preferences: { ...state.user.preferences, language },
      },
    })),
}));

// ✅ GOOD: Selective subscriptions for nested state
function UserName() {
  const name = useUserStore((state) => state.user.name); // Only re-renders when name changes
  const updateName = useUserStore((state) => state.updateName);

  return (
    <div>
      <p>Name: {name}</p>
      <input
        value={name}
        onChange={(e) => updateName(e.target.value)}
        placeholder='Enter name'
      />
    </div>
  );
}

function ThemeToggle() {
  const theme = useUserStore((state) => state.user.preferences.theme); // Only re-renders when theme changes
  const updateTheme = useUserStore((state) => state.updateTheme);

  return (
    <button onClick={() => updateTheme(theme === 'light' ? 'dark' : 'light')}>
      Current theme: {theme}
    </button>
  );
}

// ✅ GOOD: Async actions with Zustand
interface TodoState {
  todos: Todo[];
  loading: boolean;
  error: string | null;
  fetchTodos: () => Promise<void>;
  addTodo: (text: string) => void;
  toggleTodo: (id: string) => void;
}

const useTodoStore = create<TodoState>((set, get) => ({
  todos: [],
  loading: false,
  error: null,
  fetchTodos: async () => {
    set({ loading: true, error: null });
    try {
      const response = await fetch('/api/todos');
      const todos = await response.json();
      set({ todos, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
  addTodo: (text) =>
    set((state) => ({
      todos: [
        ...state.todos,
        { id: Date.now().toString(), text, completed: false },
      ],
    })),
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    })),
}));

// ✅ GOOD: Components using the async store
function TodoList() {
  const { todos, loading, error, fetchTodos } = useTodoStore();

  useEffect(() => {
    fetchTodos();
  }, [fetchTodos]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function TodoItem({ todo }: { todo: Todo }) {
  const toggleTodo = useTodoStore((state) => state.toggleTodo);

  return (
    <li>
      <input
        type='checkbox'
        checked={todo.completed}
        onChange={() => toggleTodo(todo.id)}
      />
      <span
        style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
      >
        {todo.text}
      </span>
    </li>
  );
}

Last updated