nubisite
Published on

Exploring React State Management with Zustand: Building a Simple Kanban Board Project

Introduction

State management is a crucial aspect of any modern web application, and ReactJS provides various solutions to handle this efficiently. Among these, Zustand has gained popularity for its simplicity and flexibility. In this article, we will delve into the world of Zustand by building a straightforward Kanban board. The project aims to demonstrate the power of Zustand and how it can be used to create a reactive and interactive user interface with the help of React Beautiful DND from Atlassian.

Kanban GIF

Understanding Zustand

Zustand is a state management library for ReactJS that emphasizes simplicity and performance. It allows developers to create stores that hold the application state and provides a straightforward API to access and update this state. The beauty of Zustand lies in its minimalistic setup and ability to integrate seamlessly with React components. To get started, we need to install Zustand using npm or yarn.

npm i zustand

Creating the Kanban Board

For this project, we will develop a simple Kanban board with three boards - "Todo," "In Progress," and "Done." The objective is to move tasks smoothly between these boards using drag-and-drop functionality provided by React Beautiful DND.

Infre Analysis Image

Setting Up Zustand

First, we need to set up Zustand in our project. We can create a store to hold the state for our Kanban board. The state will consist of the tasks and their statuses, such as "Todo," "In Progress," or "Done." Zustand's easy-to-use API allows us to add, remove, and update tasks efficiently. For persistency, we can use the zustand/middleware that comes with Zustand. This middleware allows us to persist the state in local storage, so that the state is not lost when the page is refreshed.

// store.js
import { create } from 'zustand';
import { v4 as uuidv4 } from 'uuid';
const idTODO = uuidv4();
const idInProgress = uuidv4();
const idDone = uuidv4();
import { persist } from 'zustand/middleware';

const store = (set) => ({
  boards: [
    {
      id: idTODO,
      title: 'To Do',
      description: 'Tasks that need to be done',
      color: 'bg-rose-500',
    },
    {
      id: idInProgress,
      title: 'In Progress',
      description: 'Tasks that are in progress',
      color: 'bg-indigo-500',
    },
    {
      id: idDone,
      title: 'Done',
      description: 'Tasks that are done',
      color: 'bg-green-500',
    },
  ],
  setBoards: (boards) => {
    set((state) => ({
      boards: boards,
    }));
  },
  deleteBoard: (id) => {
    set((state) => ({
      boards: state.boards.filter((board) => board.id !== id),
    }));
  },
  addBoard: (id, title, description, color) => {
    set((state) => ({
      boards: [...state.boards, { id, title, description, color }],
    }));
  },
  editBoard: (id, title, description, color) => {
    set((state) => ({
      boards: state.boards.map((board) => {
        if (board.id === id) {
          return { ...board, title, description, color };
        } else {
          return board;
        }
      }),
    }));
  },

  tasks: [
    {
      id: uuidv4(),
      boardId: idInProgress,
      title: 'Migration to Cloud',
      description: 'lorem',
      dueDate: '2023-5-24',
    },
    {
      id: uuidv4(),
      boardId: idInProgress,
      title: 'Security Audit',
      description: 'lorem',
      dueDate: '2023-2-1',
    },
    {
      id: uuidv4(),
      boardId: idTODO,
      title: 'Sales Pitch',
      description: 'lorem',
      dueDate: '2023-7-13',
    },
    {
      id: uuidv4(),
      boardId: idDone,
      title: 'Forum Group Discussion',
      description: 'lorem',
      dueDate: '2023-12-10',
    },
  ],
  addTask: (id, boardId, title, description, dueDate) => {
    set((state) => ({
      tasks: [...state.tasks, { id, boardId, title, description, dueDate }],
    }));
  },
  deleteTask: (id) => {
    set((state) => ({
      tasks: state.tasks.filter((task) => task.id !== id),
    }));
  },
  setTaskStatus: (id, status) => {
    set((state) => ({
      tasks: state.tasks.map((task) => {
        if (task.id === id) {
          return { ...task, status };
        } else {
          return task;
        }
      }),
    }));
  },
  setTasks: (tasks) => {
    set((state) => ({
      tasks: tasks,
    }));
  },
});

export const useStore = create(persist(store, { name: 'kanban' }));

Integrating React Beautiful DND

React Beautiful DND simplifies the process of adding drag-and-drop functionality to our Kanban board. By wrapping our Kanban boards with appropriate DND components, we can easily rearrange the tasks between different lists, giving the user a fluid and interactive experience. To use React Beautiful DND, we need to install it using npm or yarn.

npm i react-beautiful-dnd

To get started, we need to wrap our Kanban boards with the DragDropContext component. This component is responsible for managing the drag-and-drop interactions and updating the state accordingly. We can also specify a callback function to handle the state updates. In our case, we will use the onDragEnd callback to update the state when a task is dropped into a new list. The DragDropContext component also accepts a droppableId prop, which is used to identify the list that is being dragged. We can use this prop to update the state accordingly.

Infre Analysis Image

React Beautiful does not support nested drag-and-drop interactions out of the box. To enable this functionality, we need to wrap specify type to Droppable component. This component accepts a droppableId prop, which is used to identify the list that is being dragged. We can use this prop to update the state accordingly.

To make our Kanban boards draggable, we need to wrap them with the Draggable component. This component accepts a draggableId prop, which is used to identify the task that is being dragged. We can use this prop to update the state accordingly.

Finally, we need to write onDragEnd function to update the state when a task is dropped into a new list. This function accepts a result object, which contains information about the drag-and-drop interaction. We can use this object to update the state accordingly.

const filterTasks = (boardId) => {
  return tasks.filter((task) => task.boardId === boardId);
};
const filterTasks2 = (boardId) => {
  return tasks.filter((task) => task.boardId !== boardId);
};

const onDragTask = (result) => {
  const { source, destination } = result;

  if (!result.destination) {
    return;
  }
  if (source.droppableId !== destination.droppableId) {
    const column = filterTasks(source.droppableId);
    const column3 = filterTasks(destination.droppableId);
    const [removed] = column.splice(source.index, 1);
    removed.boardId = destination.droppableId;
    column3.splice(destination.index, 0, removed);
    const column4 = filterTasks2(destination.droppableId);

    setTasks([...column3, ...column4]);
  } else {
    const column = filterTasks(source.droppableId);
    const [removed] = column.splice(source.index, 1);
    column.splice(destination.index, 0, removed);
    const column2 = filterTasks2(source.droppableId);
    setTasks([...column, ...column2]);
  }
};
const onDragBoard = (result) => {
  const { source, destination } = result;
  if (!result.destination) {
    return;
  }
  const [removed] = boards.splice(source.index, 1);
  boards.splice(destination.index, 0, removed);
  setBoards([...boards]);
};
const onDragEnd = (result) => {
  const { type } = result;
  if (type !== 'drop-task') {
    onDragTask(result);
  } else {
    onDragBoard(result);
  }
};