Search code examples
javascriptreactjsnode.jstypescriptmongodb

How to Update a Kanban List Dynamically After Creating a New Task with MongoDB?


'm working on a Kanban application where I need the list of tasks to update immediately after creating a new task. I'm using MongoDB with Express and Axios, but I'm having trouble getting the list to refresh dynamically without a full page reload.

Here are the main components of my application:

1.List Component: This component fetches and displays tasks grouped by stages. It uses the useState and useEffect hooks to manage state and fetch data from the server.

type Task = {
  id: string;
  title: string;
  description: string;
  dueDate?: Date;
  completed: boolean;
  stageId?: string | null;
  users: { id: string; name: string; avatarUrl: string }[];
  createdAt: string;
  updatedAt: string;
};

type TaskStage = {
  id: string;
  title: string;
  tasks: Task[];
  createdAt: string;
  updatedAt: string;
};

const List = ({ children }: React.PropsWithChildren) => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [stages, setStages] = useState<TaskStage[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const {replace} = useNavigation();

  // Fetch tasks and stages from MongoDB
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('http://localhost:3000/');
        setTasks(response.data.tasks);
        setStages(response.data.taskStages);
      } catch (error) {
        console.error('Error fetching data:', error);
      } finally {
        setIsLoading(false);
      }
    };
    fetchData();
  }, []);
  // console.log(tasks);
  // console.log(stages);
 
  // Group tasks by stages
  const tasksStages = React.useMemo(() => {
    if (!tasks.length || !stages.length) {
      return {
        unassignedStage: [],
        columns: []
      };
    }

    const unassignedStage = tasks.filter((task: Task) => !task.stageId) ?? [];
    const groupedStages = stages.map((stage: TaskStage) => ({
      ...stage,
      tasks: tasks.filter((task: Task) => task.stageId === stage.id) ?? [],
    })) ?? [];

    return {
      unassignedStage,
      columns: groupedStages
    };
  }, [stages, tasks]);
  
  const handleTaskCreate = (newTask: Task) => {
    setTasks((prevTasks) => [...prevTasks, newTask]);
};
  // Handle adding new card by pressing the pluss icon : 
  const handleAddCard = (args: { stageId: string }) => {
    const path = args.stageId === 'unassigned' ? '/tasks/new' : `/tasks/new?stageId=${args.stageId}`;
    replace(path); // Use navigate instead of replace
  };
...
 return (
    <>
      <KanbanBoardContainer>
        <KanbanBoard onDragEnd={handleOnDragEnd}>
          <KanbanColumn
            id="unassigned"
            title="Unassigned"
            count={tasksStages.unassignedStage.length || 0}
            onAddClick={() => handleAddCard({ stageId: 'unassigned' })} description={undefined}>
            {tasksStages.unassignedStage.map((task: Task) => (
              <KanbanItem key={task.id} id={task.id} data={{ ...task, stageId: 'unassigned' }}>
                <ProjectCardMemo {...task} dueDate={String(task.dueDate) || undefined} />
              </KanbanItem>
            ))}
            {!tasksStages.unassignedStage.length && <KanbanAddCardButton onClick={() => handleAddCard({ stageId: 'unassigned' })} />}
          </KanbanColumn>

          {tasksStages.columns?.map((column: TaskStage) => (
            <KanbanColumn
              key={column.id}
              id={column.id}
              title={column.title}
              count={column.tasks.length}
              onAddClick={() => handleAddCard({ stageId: column.id })} description={undefined}>
              {column.tasks.map((task: Task) => (
                <KanbanItem key={task.id} id={task.id} data={task}>
                  <ProjectCardMemo {...task} dueDate={String(task.dueDate) || undefined} />
                </KanbanItem>
              ))}
              {!column.tasks.length && <KanbanAddCardButton onClick={() => handleAddCard({ stageId: column.id })} />}
            </KanbanColumn>
          ))}
        </KanbanBoard>
      </KanbanBoardContainer>
      {children}
    </>
  );
};

export default List;

2.TasksCreatePage Component: This component is used to create new tasks. It opens a modal form where users can input task details. Upon submission, the task is created, but I need to find a way to ensure the new task appears in the list immediately.

import { useSearchParams } from "react-router-dom";
import { useModalForm } from "@refinedev/antd";
import { useNavigation } from "@refinedev/core";
import { Form, Input, Modal } from "antd";
import axios from "axios";

const CreateTask = () => {
  const [searchParams] = useSearchParams();
  const { list } = useNavigation();

  const { formProps, modalProps, close } = useModalForm({
    action: "create",
    defaultVisible: true,
  });

  const onSubmit = async (values: any) => {
    const stageId = searchParams.get("stageId") || null;

    try {
      const response = await axios.post('http://localhost:3000/tasks', {
        title: values.title,
        stageId,
        completed: false,
        users: [],
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      });

      if (response.status === 201) {
        // Optionally show a success message here
        list("tasks", "replace");
      }
    } catch (error) {
      console.error('Error creating task:', error);
    }

    close();
  };

  return (
    <Modal
      {...modalProps}
      onCancel={() => {
        close();
        list("tasks", "replace");
      }}
      title="Add new card"
      width={512}
    >
      <Form {...formProps} layout="vertical" onFinish={onSubmit}>
        <Form.Item label="Title" name="title" rules={[{ required: true }]}>
          <Input />
        </Form.Item>
      </Form>
    </Modal>
  );
};

export default CreateTask;

3.App Component: This component manages routing in the application and includes a route to TasksCreatePage.

 <Route path="/tasks" element={<List>
                          <Outlet/>
                        </List>}>
                          <Route path="new" element={<CreateTask/>}/>
                          <Route path="edit/:id" element={<EditTask/>}/>
                        </Route>

How can I update the List component dynamically when a new task is created using MongoDB? I would appreciate any guidance or examples on how to achieve this functionality in a React application.


Solution

  • From my understanding, a solution that would work for this would be to use a Context Provider. https://react.dev/reference/react/useContext

    You can create a TaskContext which will allow tasks and stages to be globally available.

    import React, { createContext, useContext, useState } from 'react';
    
    // Create a context for task-related actions
    const TaskContext = createContext(null);
    
    export const useTaskContext = () => useContext(TaskContext);
    
    // Create a provider for tasks and stages
    export const TaskProvider = ({ children }) => {
      const [tasks, setTasks] = useState<Task[]>([]);
      const [stages, setStages] = useState<TaskStage[]>([]);
    
      const handleTaskCreate = (newTask: Task) => {
        setTasks((prevTasks) => [...prevTasks, newTask]);
      };
    
      return (
        <TaskContext.Provider value={{ tasks, stages, handleTaskCreate }}>
          {children}
        </TaskContext.Provider>
      );
    };
    

    You can then update your List component and CreateTask component to utilize the TaskContext, and update dependency array for useEffect

    import { useTaskContext } from './TaskContext';
    
    const List = ({ children }) => {
      const { tasks, setTasks, stages, setStages } = useTaskContext();
    
      // Fetch tasks and stages from MongoDB
      useEffect(() => {
        const fetchData = async () => {
          const { data } = await axios.get('http://localhost:3000/');
          setTasks(data.tasks);
          setStages(data.taskStages);
        };
        fetchData();
      }, [setTasks, setStages]);
    
      // Group tasks by stages
      const tasksStages = useMemo(() => {
        const unassignedStage = tasks.filter(task => !task.stageId);
        const groupedStages = stages.map(stage => ({
          ...stage,
          tasks: tasks.filter(task => task.stageId === stage.id),
        }));
        return { unassignedStage, columns: groupedStages };
      }, [tasks, stages]);
    
      return (
        <>
          /* Kanban Board */
          {children}
        </>
      );
    };
    
    
    import { useTaskContext } from './TaskContext';
    
    const CreateTask = () => {
      const { handleTaskCreate } = useTaskContext();
      const [title, setTitle] = useState('');
    
      const onSubmit = (e) => {
        e.preventDefault();
        const newTask = {
          id: String(Date.now()),
          title,
          completed: false,
          stageId: null,
        };
        handleTaskCreate(newTask);
      };
    
      return (
        <form onSubmit={onSubmit}>
          <input value={title} onChange={(e) => setTitle(e.target.value)} />
          <button type="submit">Create Task</button>
        </form>
      );
    };
    

    And finally update App.js and wrap your app with TaskContext to make it acessable to all components.

    {/* previous imports */}
    import { TaskProvider } from './TaskContext'; // Import TaskProvider
    
    function App() {
      return (
        <Routes>
          <Route
            path="/tasks"
            element={
              // Wrap TaskProvider around List and its nested routes
              <TaskProvider>
                <List>
                  <Outlet />
                </List>
              </TaskProvider>
            }
          >
            <Route path="new" element={<CreateTask />} />
            <Route path="edit/:id" element={<EditTask />} />
          </Route>
        </Routes>
      );
    }
    

    hope this helps!