User Guide for Zustand

Introduction

Zustand is an extremely lightweight, fast, and easy-to-use state management library for React applications. Highlighted advantages of Zustand include:

  • No need for a Provider to wrap the application
  • Extremely concise syntax that reduces boilerplate code
  • Excellent support for TypeScript
  • High performance thanks to a mechanism that only re-renders necessary components.

Detail

First, install it into your project as follows:

yarn add zustand


Let's start with a simple Counter app example for you to easily understand how to use it.

Create file useCounterStore.ts

import {create} from 'zustand'

interface CounterState {
count: number
increment: () => void
decrement: () => void
reset: () => void
}

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

export default useCounterStore

The code block above initializes a store to manage the count variable with a default value of 0, and defines 3 control functions: increment, decrement, and reset the value of count via Zustand's set function.


Create file page.tsx to use that store

import {Button, Space, Typography} from 'antd'
import useCounterStore from './useCounterStore'

const {Title} = Typography

const CounterComponent = () => {
const {count, increment, decrement, reset} = useCounterStore()

return (
<div className="flex flex-col items-center justify-center mt-12 gap-6">
<Title level={1} className="!m-0 text-slate-800">
Count: <span className="text-blue-600">{count}</span>
</Title>

<Space
size="middle"
className="bg-white p-6 rounded-xl shadow-sm border border-slate-100"
>
<Button
type="primary"
danger
onClick={decrement}
className="hover:scale-105 transition-transform"
>
Decrement -
</Button>

<Button
onClick={reset}
className="hover:border-blue-400 hover:text-blue-400 transition-colors"
>
Reset
</Button>

<Button
type="primary"
onClick={increment}
className="bg-blue-600 hover:!bg-blue-500 hover:scale-105 transition-transform"
>
Increment +
</Button>
</Space>
</div>
)
}

export default CounterComponent

This is the user interface using Ant Design to display the count value from the store and assign the increment, decrement, and reset functions to the corresponding buttons to update the state instantly on the UI.


The result will be as follows:


Next, I will guide you on using Zustand middleware in combination with some popular tools such as:

  • devtools: supports debugging with Redux Devtools
  • persist: saves data to localStorage
  • immer: allows updating data in a mutable style for object data types; you don't need to return a new object as usual


Install immer additionally

yarn add immer


Create file .env with the following content

NEXT_PUBLIC_DEV = false

Note that because I use it in a NextJS project, a NEXT_PUBLIC prefix is needed for frontend use; if you are using Vite, this is not necessary.


Create file useTodoStore.ts

import {create} from 'zustand'
import {devtools, persist} from 'zustand/middleware'
import {immer} from 'zustand/middleware/immer'

interface Todo {
id: string
text: string
completed: boolean
}

interface TodoState {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: string) => void
deleteTodo: (id: string) => void
clearCompleted: () => void
}

const enabled = process.env.NEXT_PUBLIC_DEV === 'true'

const useTodoStore = create<TodoState>()(
devtools(
persist(
immer(set => ({
todos: [],

addTodo: text =>
set(state => {
state.todos.push({
id: crypto.randomUUID(),
text,
completed: false,
})
}),

toggleTodo: id =>
set(state => {
const todo = state.todos.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}),

deleteTodo: id =>
set(state => {
state.todos = state.todos.filter(t => t.id !== id)
}),

clearCompleted: () =>
set(state => {
state.todos = state.todos.filter(t => !t.completed)
}),
})),
{
name: 'todo-storage',
}
),
{name: 'TodoStore', enabled}
)
)

export default useTodoStore

This code block sets up a store for the Todo application.

  • You can see that functions like addTodo and toggleTodo used with immer can directly modify the state object without needing to return a new object.
  • The name: 'todo-storage' part is the key for saving to Local Storage.
  • The enabled variable will take its value from the .env file defined above to only enable debug mode in development mode; when deploying to a production environment, it will be disabled.


Create file page.tsx to use useTodoStore

import {
DeleteOutlined,
DeleteRowOutlined,
PlusOutlined,
} from '@ant-design/icons'
import {
Button,
Card,
Checkbox,
Divider,
Empty,
Flex,
Input,
Space,
Tag,
Typography,
} from 'antd'
import {useState} from 'react'
import useTodoStore from './useTodoStore'

const {Title, Text} = Typography

const TodoApp = () => {
const [inputValue, setInputValue] = useState('')
const {todos, addTodo, toggleTodo, deleteTodo, clearCompleted} =
useTodoStore()

const handleAdd = () => {
if (inputValue.trim()) {
addTodo(inputValue.trim())
setInputValue('')
}
}

return (
<div className="min-h-screen bg-slate-50 py-12 px-5">
<Card className="max-w-[600px] mx-auto rounded-xl shadow-md border-none">
<Flex vertical gap="large">
<div className="text-center">
<Title level={2} className="!m-0">
🎯 Todo App
</Title>
<Text className="text-slate-400">Zustand + Immer + Persist</Text>
</div>

<Flex gap="small">
<Input
size="large"
placeholder="What needs to be done?"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onPressEnter={handleAdd}
className="hover:border-blue-400 focus:border-blue-500"
/>
<Button
size="large"
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
className="bg-blue-600 hover:bg-blue-500"
>
Add
</Button>
</Flex>

<Flex justify="space-between" align="center" className="px-1">
<Space size="middle">
<Tag color="blue" className="px-3 rounded-full">
Total: {todos.length}
</Tag>
<Tag color="green" className="px-3 rounded-full">
Done: {todos.filter(t => t.completed).length}
</Tag>
</Space>

{todos.some(t => t.completed) && (
<Button
type="link"
danger
size="small"
icon={<DeleteRowOutlined />}
onClick={clearCompleted}
className="hover:opacity-80 transition-opacity"
>
Clear all done
</Button>
)}
</Flex>

<Divider className="my-2" />

<Flex
vertical
gap="small"
className="max-h-[500px] overflow-y-auto pr-1"
>
{todos.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<span className="text-slate-400">Your list is empty</span>
}
/>
) : (
todos.map(item => (
<Card
key={item.id}
size="small"
hoverable
styles={{body: {padding: '12px'}}}
className={`transition-all duration-300 border ${
item.completed
? 'bg-slate-50 border-slate-100'
: 'bg-white border-slate-200'
}`}
>
<Flex justify="space-between" align="center">
<Checkbox
checked={item.completed}
onChange={() => toggleTodo(item.id)}
>
<Text
delete={item.completed}
className={`ml-2 text-base transition-colors ${
item.completed ? 'text-slate-400' : 'text-slate-700'
}`}
>
{item.text}
</Text>
</Checkbox>

<Button
type="text"
danger
shape="circle"
icon={<DeleteOutlined />}
onClick={() => deleteTodo(item.id)}
className="hover:bg-red-50 flex items-center justify-center"
/>
</Flex>
</Card>
))
)}
</Flex>
</Flex>
</Card>
</div>
)
}

export default TodoApp

This is the main UI component of the Todo application, performing the connection of user actions (entering text, clicking the add button, checking completion, deleting) with the corresponding data processing functions in useTodoStore to manage the task list.


Checking the results will show that data has been saved in local storage, so you can reload the page and still keep the data.


When you change the state, it will be shown in the corresponding Redux Devtools.

Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Using Kafka with Docker and NodeJS

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

NodeJS Practice Series

Sitemap