DAY 37: Mastered Zustand From Basics to Full CRUD Todo App Implementation | ReactJS


🚀 Introduction
Welcome to Day 37 of my Web Development Journey!
After building a solid foundation in HTML, CSS, and JavaScript, I’ve been diving into ReactJS — a powerful library for creating dynamic, interactive user interfaces.
Over the past few days, I started learning Zustand for React state management — super simple yet incredibly powerful! I explored how to create a store, access state, update state, use middlewares, and handle async actions.
To stay consistent and share my learnings, I’m documenting this journey publicly. Whether you’re just starting with React or need a refresher, I hope this blog offers something valuable!
Feel free to follow me on Twitter for real-time updates and coding insights.
📅 Here’s What I Covered Over the Last 3 Days
Day 34
- Deep dive into Zustand
- Installing Zustand
- Creating a store
- Accessing state in components
- Updating state
Day 35
- Accessing state outside components (
get
) - Middlewares in Zustand
- Common built-in middlewares
Day 36
- Derived state
- Async actions
- Best practices
- Built a Todo App with full CRUD functionality
Let’s dive into these concepts and the app in more detail below 👇
1. Zustand:
Zustand is a small, fast, and scalable state management library for React applications. It offers a simple and intuitive API to manage global state without the boilerplate typically associated with other libraries like Redux.
Key Features of Zustand:
Minimalistic and Lightweight:
Zustand has a tiny footprint and minimal setup, making it easy to integrate into any React project without overhead.No Boilerplate:
Unlike Redux, Zustand avoids complex actions, reducers, and dispatch mechanisms. Instead, it uses simple functions to create and manipulate state.Direct State Access:
Components can subscribe to specific parts of the state, ensuring efficient re-rendering only when relevant state changes occur.Built-in Support for Middleware:
Zustand supports middlewares out of the box, which can be used for logging, persistence, or handling asynchronous logic.Works Seamlessly with React Hooks:
Zustand leverages React’s hooks API, providing a natural and modern way to manage state in functional components.Flexible and Scalable:
It can handle simple use cases with a single store as well as complex applications with multiple stores.
Installing Zustand:
Zustand is available as a package on NPM for use:
# NPM
npm install zustand
# Or, use any package manager
Creating a Store:
In Zustand, a store is where you define your application’s state and the functions to update that state.
It acts like a centralized place to keep and manage your state, making it accessible across components without prop drilling or complex setups.
store is a hook, We can put anything in it : primitives
, objects
, functions
.
The set function merges state.
Here’s a simple example where we create a counter store using Zustand’s create
function:
import create from "zustand";
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
Explanation:
- We import the
create
function from Zustand. - The
create
function takes a function as an argument. This function returns an object that represents the initial state of the store — including both state properties and actions (functions to update the state). useCounterStore
is the custom hook created bycreate
, which holds this state and actions.- In this example, the state includes a
count
property initialized to0
. - We define two actions,
increment
anddecrement
, which update thecount
using theset
method provided by Zustand. - By calling
useCounterStore
inside a React component, we can easily access the current state and invoke these actions to update it.
Accessing store in components:
To use the Zustand store in our React components, we simply call the custom hook created by create
(e.g., useCounterStore
). This hook lets us:
- Access the current state values.
- Call actions to update the state.
Here’s an example of accessing and using the count
state along with the increment
and decrement
actions inside a React component:
import { useCounterStore } from "./store";
const Counter = () => {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
export default Counter;
- The selector function
(state) => state.count
lets us pick only the part of the state we need. - This approach helps optimize component re-renders by subscribing only to the relevant parts of the store.
- We can access multiple state values and actions by calling the hook multiple times or destructuring them together.
Updating State in Zustand:
There are two main ways to update state in Zustand stores: Functional Update and Direct Update. Let's explore both using a simple counter example.
Functional Update
In this method, we pass a function to the set
method that receives the current state and returns a new partial state. This approach is useful when the new state depends on the previous state.
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
- Here,
set
takes a function(state) => ({ count: state.count + 1 })
. - This function receives the current state and returns an updated state object.
- It ensures we always work with the latest state, avoiding bugs in concurrent updates.
Direct Update
We can also update state by passing an object directly to set
when the new state does not depend on the previous state.
const useCounterStore = create((set) => ({
count: 0,
reset: () => set({ count: 0 }),
}));
- Here,
set
receives an object{ count: 0 }
that directly replaces the relevant part of the state. - This method is straightforward for simple assignments or resets.
Accessing state outside components (get
):
Sometimes, we may need to access the store's state outside of React components, such as in utility functions, event listeners, or API calls.
Zustand provides the get
method for this purpose.
import { create } from 'zustand';
const useCounterStore = create((set, get) => ({
count: 0,
increment: () => set({ count: get().count + 1 }),
}));
// Accessing state outside a component
console.log(useCounterStore.getState().count); // 0
// Updating state outside a component
useCounterStore.getState().increment();
console.log(useCounterStore.getState().count); // 1
Explanation:
- We define our store with two arguments in the
create
function:set
→ for updating stateget
→ for reading the current state
get()
allows us to read the store's state anywhere, even outside React.- In this example:
useCounterStore.getState().count
fetches the current value ofcount
.- We can call actions (like
increment
) directly from the store.
- This is especially useful for logic that doesn’t directly involve a React component.
Middlewares in Zustand:
Middlewares in Zustand are higher-order functions that enhance our store with extra capabilities such as persistence, logging, or devtools integration.
They wrap around our store definition and intercept state updates or actions to provide additional functionality.
Syntax:
import { create } from "zustand";
import { middlewareName } from "zustand/middleware";
const useStore = create(
middlewareName((set, get) => ({
// state
count: 0,
// actions
increment: () => set(state => ({ count: state.count + 1 }))
}))
);
Common Built-in Middlewares:
- persist:
- Saves our store state to localStorage or another storage mechanism.
- Automatically rehydrates the store state on page reload.
import { persist } from "zustand/middleware";
const useCounterStore = create(
persist(
(set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}),
{
name: "counter-storage", // key in localStorage
}
)
);
- devtools:
- Integrates our store with Redux DevTools for easier debugging.
- Lets us track state changes and dispatched actions.
import { devtools } from "zustand/middleware";
const useCounterStore = create(
devtools((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}), { name: "CounterStore" })
);
Middlewares make Zustand stores more powerful and easier to manage, especially for persistence, debugging, and logging state changes.
Derived State (Computed Values):
Derived state, also called computed state, is a value that depends on the store's existing state but is calculated dynamically rather than stored directly. This helps avoid redundant state and keeps your store clean.
Example: Double Counter Value
import create from "zustand";
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
double: () => {
const state = useCounterStore.getState();
return state.count * 2;
},
}));
Explanation:
count
is the primary state.increment
anddecrement
update thecount
.double
is a derived state computed fromcount
without storing it separately.- Calling
useCounterStore.getState().double()
returns twice the currentcount
.
This approach ensures that derived values always reflect the latest state without introducing unnecessary stored properties.
Async Actions in Zustand:
Zustand allows us to define asynchronous actions directly in our store, which is useful for fetching data or performing other async tasks.
In this example, we have a useUserStore
that manages a list of users:
import { create } from "zustand";
export const useUserStore = create((set) => ({
users: [],
loading: false,
error: "",
fetchUsers: async (url) => {
if (!url) {
set({ error: "Url is Required!" });
return;
}
try {
set({ loading: true, error: "" });
const res = await fetch(url);
if (!res.ok) throw new Error("Failed to fetch users");
const data = await res.json();
if (!data.length) {
set({ error: "No data Found", loading: false });
return;
}
set({ users: data, loading: false });
} catch (err) {
set({ error: err.message, loading: false });
}
},
}));
Explanation:
users
,loading
, anderror
are part of the store's state.fetchUsers
is an async action that accepts aurl
to fetch data from.- Before fetching, it sets
loading: true
and clears any previous errors. - The
fetch
call retrieves data from the API, andset
updates the state accordingly. - If the URL is missing, the response fails, or no data is returned,
error
is set with a descriptive message. - Once the data is successfully fetched,
users
is updated andloading
is set tofalse
.
This pattern allows us to manage async state changes seamlessly within Zustand, keeping your components clean and reactive.
When to Use Zustand?
Zustand is a great choice in the following scenarios:
- Simple Global State Management – When we need a lightweight solution without the boilerplate of Redux.
- Intermediate React Projects – Ideal for apps that need more than
useState
oruseReducer
but don’t require a full-scale state management library. - Async Data Fetching – When managing API calls and loading/error states across components.
- Derived or Computed State – When we need values computed from the store without duplicating state.
- Debugging & Persistence Needs – With middlewares like
persist
anddevtools
, it becomes easy to debug and persist state across sessions.
Best Practices with Zustand:
- Keep Stores Small and Focused – Create multiple small stores for different domains instead of one giant store.
- Use Selectors – Access only the state we need in components to avoid unnecessary re-renders.
- Leverage Middlewares Wisely – Use
persist
for saving state,devtools
for debugging, but avoid overusing middlewares that aren’t necessary. - Organize Async Actions Clearly – Keep fetches and async logic inside the store to maintain clean components.
- Avoid Storing Derived State – Compute values on-the-fly using derived state instead of storing them redundantly.
- Use
get
for Non-Component Logic – Access store state outside components when needed without triggering re-renders.
Final Thoughts:
- Zustand is a lightweight, flexible, and powerful state management library for React.
- It simplifies state handling by providing a clean API with create, set, and get, while supporting middlewares, derived state, and async actions.
- Using Zustand allows us to manage both simple and complex state efficiently, keeping our components clean.
- It helps avoid the boilerplate of other state management solutions.
- By following best practices—like keeping stores focused, using selectors, and leveraging middlewares wisely—we can build scalable and maintainable React applications with ease.
2. Implementing CRUD Functionality in Todo App Using Zustand:
In this section, I’ll explain how I leveraged Zustand to manage the state of a Todo App. The focus is mainly on how the store is structured, how state and actions are accessed in components, and how the app handles CRUD operations efficiently.
1. Creating the Store
The useTodoStore
is our main Zustand store. It uses the persist
middleware to save todos in localStorage
, so the state remains even after a page refresh.
Key points of the store:
- State:
todos
: an array of todo objects{ id, name, completed }
.
- Actions:
addTodo
: Adds a new todo with a unique ID andcompleted: false
.toggleTodo
: Toggles thecompleted
state of a specific todo.deleteTodo
: Removes a todo by its ID.editTodo
: Updates the name of a specific todo.
Code:
export const useTodoStore = create(
persist(
(set) => ({
todos: [],
addTodo: (todo) => {
set((state) => ({
todos: [
...state.todos,
{
id: crypto.randomUUID(),
name: todo,
completed: false,
},
],
}));
},
toggleTodo: (id) => {
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}));
},
deleteTodo: (id) => {
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
}));
},
editTodo: (todoName, id) => {
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, name: todoName } : todo
),
}));
},
}),
{
name: "todos",
storage: createJSONStorage(() => localStorage),
}
)
);
Explanation:
create
initializes the store with state and actions.persist
middleware saves the store inlocalStorage
.set
is used to update the state immutably.- Each action (
addTodo
,toggleTodo
,deleteTodo
,editTodo
) updates thetodos
array appropriately.
2. Accessing State in Components
We use the useTodoStore
hook inside React components to read state or dispatch actions.
Zustand’s selector function allows subscribing to only the part of the state needed, which improves performance.
Example in the main component:
const todos = useTodoStore((state) => state.todos);
This fetches the current
todos
array.Components like
TodoList
andTodoItem
also use selectors to pick only the actions they need:const toggleTodo = useTodoStore((state) => state.toggleTodo); const deleteTodo = useTodoStore((state) => state.deleteTodo);
3. GitHub Repository:
Explore the full source code of this Todo App on GitHub: Todo App Repository
3. What’s Next:
I’m excited to keep growing and sharing along the way! Here’s what’s coming up:
- Posting new blog updates every 3 days to share what I’m learning and building.
- Diving deeper into Data Structures & Algorithms with Java — check out my ongoing DSA Journey Blog for detailed walkthroughs and solutions.
- Sharing regular progress and insights on X (Twitter) — feel free to follow me there and join the conversation!
Thanks for being part of this journey!
4. Conclusion:
In this blog, we explored Zustand in depth — from creating stores and updating state to using middlewares, derived state, and async actions.
We also implemented a Todo App with full CRUD functionality, demonstrating how Zustand enables efficient state management and clean component subscriptions.
Key Takeaways:
- Zustand offers a simple and flexible approach to managing state in React applications.
- Its selector functions and middlewares optimize performance and enable state persistence.
- Using Zustand for CRUD operations shows how to maintain clean and reactive state without prop drilling or boilerplate.
- With a solid understanding of both theory and practical usage, we can confidently integrate Zustand into your React projects.
💻 Explore the full source code on GitHub: Todo App Repository
Thanks for reading! Feel free to connect or follow along as I continue building and learning in React.
Subscribe to my newsletter
Read articles from Ritik Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ritik Kumar
Ritik Kumar
👨💻 Aspiring Software Developer | MERN Stack Developer.🚀 Documenting my journey in Full-Stack Development & DSA with Java.📘 Focused on writing clean code, building real-world projects, and continuous learning.