.Djibril MUG

React state management with Zustand🐻

avatar

Djibril Mug

8 Aug 2023

7 min read


React state management with Zustand🐻

With all the state management libraries that we currently have, deciding what to use for your next project can be douting, but if you want efficiency without complexity, then Zustand is probably your choice. In this article, we shall discuss some advantages provided by Zustan, and how to introduce Zustand in your React project.

###Why zustand Zustand is not the only good state management library out there, and we have some of them that have been in the field for so long, like Redux and Mobx, but what makes it special? And why is it probably the next for your next project? Here are 3 reasons why Zustand matters and why you should opt for it!

Minimalism and simplicity : Zustand was designed to be simple with a minimal approach, making it easy to learn and use.

Small size bundle: with only 1.16kb of bundle size, Zustand is by far the smallest react state management library and yet one of the performers.

selective component subscription : Zustand allow components to only subscribe state they need, which improves performance by avoiding unnecessary re-render

I think now you are convinced, and can't wait to see what Zunstand has for you. For demonstrating all those amazing features we shall be building one Todo application that will showcase the fundamentals of Zustand

Project setup

we shall use vite and tailwind.css for applying some basics styling, you can go with any CSS framework you want or just use plain CSS. Please note that everything will be built with Typescript.

For saving time and focus on Zustand, I created a simple starter project that contains all installed dependencies and a basic todo UI here is the link to the starter project repository

If you downloaded the starter project I'm sure you found an App.tsx file and Todo.tsx file, in the App.tsx file we got an input text that will be used for creating todos and a list element containing our todos. The Todo.tsx contains the code for rendering a todo and two buttons, one for deleting the todo, another one for updating the todo, and the last one that will be used for updating the status of the todo

Zustand store

Zustand embraces the idea of actions for updating states, Zustand actions are different from Redux, since actions in Zustand are just properties of the store object. Zustand has several ways of managing stores, but in this article, we shall focus on the easiest way, use the following resource for more information

Todo store

import { create } from "zustand";

export interface Todo {
    id: string,
    completed: boolean,
    content: string,
}


interface state {
    todos: Todo[],
    length: number,
}

interface Actions {
    addTodo: (newTodo: Todo) => void,
    markCompleted: (id: string) => void;
    deleteTodo: (id: string) => void,
    clearAllCompleted: () => void,
    updateTodo: (id: string, content: string) => void
}

const useTodoStore = create<state & Actions>((set, get) => ({
    todos: [],
    length: 0,
    addTodo: (newTodo) => set({ todos: [newTodo, ...get().todos,], length: get().todos.length }),
    markCompleted: (todoId) => {

        const getTodos = get().todos  // get the list of all todos avalaible in the store

        const todoIndex = getTodos.findIndex((todo) => todo.id === todoId);  //find todo index

        getTodos[todoIndex].completed = !getTodos[todoIndex].completed;  //update the complete property by its oposite.

        return set({ todos: [...getTodos], length: get().todos.length });  //update the list all todos in the store with the new changes.

    },

    deleteTodo: (todoId) => set({ todos: get().todos.filter((todo) => todo.id !== todoId) }),  //delete the targeted todo.

    clearAllCompleted: () => set({ todos: [] }),

    updateTodo: (id: string, content: string) => {
        const getTodos = get().todos; //get the list of all todos

        const todoIndex = getTodos.findIndex((td) => td.id === id); //get the targeted todo index

        getTodos[todoIndex].content = content;

        return set({ todos: [...getTodos] });
    }

}))

export default useTodoStore;

I know you have one million questions about the above code, but don't worry I have an explanation for you, the three interfaces are types of data available within the store, the create method used for creating stores provides two methods, a set method for updating states and a get method for getting available states in the store, the get method is not the only option for the getting state available in the store here is another way

const  store  =  create((set)  =>  ({
    name:  "may be your name",
    getName:  set((state)  =>  state.name)  //access state from set state
}))

Data consumption

After creating our lovely store let's consume its state now, we shall start by creating a method for creating todos and then printing them.

App.tsx file

import { useRef } from 'react'
import Todo from './components/Todo'
import useTodoStore from './store/features/todo'
import { Todo as TodoType } from './store/features/todo';


function App() {
  const addTodoAction = useTodoStore((state) => state.addTodo);
  const todos = useTodoStore((state) => state.todos);
  const input = useRef<HTMLInputElement>();

  const addTodo = () => {
    if (!input.current?.value) return;

    const newTodo: TodoType = {
      content: input.current.value,
      completed: false,
      id: new Date().toISOString(),
    }
    addTodoAction(newTodo);
    input.current.value = "";
  }


  return (
    <main className=' w-full p-2 min-h-screen bg-slate-200 pt-10'>
      <div className=" bg-white border m-auto  max-w-4xl min-h-[300px] ">
        <div className="">
          <h3 className=' text-2xl font-bold p-5 border-b bg-slate-100 text-slate-600'>Simple Todo📝</h3>
        </div>

        <div className="w-full flex   gap-5 p-5">
          <input ref={(el: HTMLInputElement) => input.current = el} placeholder=' Your todo' type="text" className='  px-4 w-full rounded-md border outline-slate-300' />
          <button onClick={() => addTodo()} className=' bg-slate-700 text-white px-10 py-4 active:bg-slate-800 rounded-md '>submit</button>
        </div>

        <ul className=' p-5'>
          {todos.map((td) => <Todo content={td.content} completed={td.completed} id={td.id} key={td.id} />)}
        </ul>
      </div>
    </main>
  )
}

export default App

If you paid attention to details, you probably notice that I did not use any extra hook or a selector for using the store, that is because Zustand is a direct state management library, with Zustand you don't need any extra hook for accessing the store's state, and the store can be accessed outside of the react tree.

Finish the todo app

In this last section of the article, we shall create all the remaining functions, like the one for deleting, updating, and marking todos as completed or not.

Todo.tsx file

import { BsTrash3 } from 'react-icons/bs';
import { MdModeEditOutline } from 'react-icons/md';
import React, { FormEvent, useState, } from 'react';
import useTodoStore from '../store/features/todo';


interface Props {
    content: string,
    completed: boolean,
    id: string
}

const Todo: React.FC<Props> = ({ content, completed, id }) => {
    const [editMode, setEditMode] = useState<boolean>(false);
    const [updateValue, setUpdateValue,] = useState<string>(content);


    //actions from useTodoStore
    const { deleteTodo, updateTodo, markCompleted } = useTodoStore((state) => state);


    //function for updating a todo
    const editTodFunction = (e: FormEvent) => {
        e.preventDefault();
        if (updateValue.trim().length < 1) return //check if the input is empty

        updateTodo(id, updateValue);
        setEditMode(false);
    }


    return (
        <li className=' w-full my-2 cursor-pointer  border flex gap-3 items-center bg-slate-50 p-5 rounded-md'>
            {
                editMode ?
                    <div className="w-full border ">
                        <form onSubmit={editTodFunction} action="">
                            <input autoFocus onChange={(e) => setUpdateValue(e.target.value)} type="text" defaultValue={content} className=' bg-transparent text-slate-500 h-10 w-full outline-none p-2' />
                        </form>
                    </div>

                    : <div className=" w-full">
                        <p style={completed ? { textDecoration: "line-through" } : {}} className=' text-slate-600'> {content} </p>
                    </div>
            }


            <div className=" flex gap-7">
                <button onClick={() => deleteTodo(id)} className=' p-3  rounded-full box-border w-12 h-12 flex items-center justify-center bg-slate-200 hover:bg-slate-300 hover:text-slate-700'>
                    <BsTrash3 className=" cursor-pointer" />
                </button>

                <button onClick={() => setEditMode(!editMode)} className=' p-3  rounded-full box-border w-12 h-12 flex items-center justify-center bg-slate-200 hover:bg-slate-300 hover:text-slate-700'>
                    <MdModeEditOutline className=" text-slate-500" />
                </button>

                <button onClick={() => markCompleted(id)} className=' p-3  rounded-md box-border bg-slate-200 text-sm hover:bg-slate-300 hover:text-slate-700'>
                    Completed
                </button>
            </div>
        </li>
    )
}


export default Todo

Bonus(persisting data)

What is a todo application without persisting data 🥱 Zustand allows stores to persist data not just in the local storage but with any other storage mechanism that the browser provides, in the example below we transform our todo store into a store that persists its data in the local storage, you can be very creative and use other storage like the indexDB, here is a link to the complete resource


const useTodoStore = create(persist<state & Actions>(
    (set, get) => ({
        todos: [],
        length: 0,
        addTodo: (newTodo) => set({ todos: [newTodo, ...get().todos,], length: get().todos.length }),
        markCompleted: (todoId) => {

            const getTodos = get().todos  // get the list of all todos avalaible in the store

            const todoIndex = getTodos.findIndex((todo) => todo.id === todoId);  //find todo index

            getTodos[todoIndex].completed = !getTodos[todoIndex].completed;  //update the complete property by its oposite.

            return set({ todos: [...getTodos], length: get().todos.length });  //update the list all todos in the store with the new changes.

        },

        deleteTodo: (todoId) => set({ todos: get().todos.filter((todo) => todo.id !== todoId) }),  //delete the targeted todo.

        clearAllCompleted: () => set({ todos: [] }),

        updateTodo: (id: string, content: string) => {
            const getTodos = get().todos; //get the list of all todos

            const todoIndex = getTodos.findIndex((td) => td.id === id); //get the targeted todo index

            getTodos[todoIndex].content = content;

            return set({ todos: [...getTodos] });
        }

    }),
    {
        name: 'food-storage', // name of the item in the storage (must be unique)
        storage: createJSONStorage(() => localStorage), // (optional) by default, 'localStorage' is used
    }
))

export default useTodoStore;

And that is it. That was a little introduction to Zustand, Zustand is not only about what I have shown you in this article, you can go beyond that, you can maybe learn how to fetch data correctly... there is always room for growth 😇

Thanks for reading 🙏, if you want some cool stuff just leave a comment, don't forget to like and follow for more contents like this.

Link to the full code