0

If I add tasksMap to the useEffect dependency array below an infinite loop will happen. Can someone point me in the right direction on how to fix this? In order for the user to get an updated view of the tasks that have been added or modified, I need the app to call getProjectTasks and assign the returned map to the tasksMap. I do know that anytime you update state the component rerenders. I just havne't figured out how to do this without creating an infinite loop. Any help is greatly appreciated. Thank you.

import { useContext, useState, useEffect } from "react";
import { useParams } from "react-router-dom";

import { UserContext } from "../../contexts/user.context";
import { ProjectsContext } from "../../contexts/projects.context";
import { createProjectTask, getProjectTasks } from "../../utils/firebase/firebase.utils";
import OutlinedCard from "../../components/cards/TaskCard.component";

import { TextField, Button, } from "@mui/material";
import "./project.styles.css";

import "./project.styles.css";

const Project = () => {
  const params = useParams();
  const projectId = params.id;

  const { currentUser } = useContext(UserContext);
  const { projectsMap } = useContext(ProjectsContext);
  const [taskName, setTaskName] = useState("");
  const [tasksMap, setTasksMap] = useState({});

  const project = Object.keys(projectsMap)
    .filter((id) => id.includes(projectId))
    .reduce((obj, id) => {
      return Object.assign(obj, {
        [id]: projectsMap[id],
      });
    }, {});

  useEffect(() => { 
    console.log("running")
    const getTasksMap = async () => {
        const taskMap = await getProjectTasks(currentUser, projectId);
        taskMap ? setTasksMap(taskMap) : setTasksMap({});
       };
       getTasksMap();
  }, [projectId])
    

  const handleChange = (event) => {
    const { value } = event.target;
    setTaskName(value);
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    try {
      await createProjectTask(currentUser, projectId, taskName);
      setTaskName("");
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <div className="project-container">
      {project[projectId] ? <h2>{project[projectId].name}</h2> : ""}

      <form onSubmit={handleSubmit} className="task-form">
        <TextField label="Project Task" onChange={handleChange} value={taskName}></TextField>
        <Button type="submit" variant="contained">
          Add Task
        </Button>
      </form>
      <div className="tasks-container">
      {Object.keys(tasksMap).map((id) => {
          const task = tasksMap[id];
          return (
              <OutlinedCard key={id} projectId={projectId} taskId={id} name={task.name}></OutlinedCard>
          );
        })}
      </div>
    </div>
  );
};
export default Project;

This is where the taskMap object comes from. For clarification, I'm using Firebase.

export const getProjectTasks = async(userAuth, projectId) => {
    if(!userAuth || !projectId) return;
    const tasksCollectionRef = collection(db, "users", userAuth.uid, "projects", projectId, "tasks")
    const q = query(tasksCollectionRef);
    try {
      const querySnapshot = await getDocs(q);
      const taskMap = querySnapshot.docs.reduce((acc, docSnapshot) => {
        const id = docSnapshot.id;
        const { name } = docSnapshot.data();
        acc[id] = {id, name};
        return acc;
      }, {});
      return taskMap;
    } catch (error) {
      console.log("Error getting task docs.");
    }
};
Rocket455
  • 75
  • 4
  • Responding to comment in answer below. Can you clarify what "My logic is with the tasksMap added to the dependency array, the user will be kept up to date when they add a task or edit/delete a task such as marking it as complete." means? What is a user updating that you need to refresh the page to see? Or rather, why is updating the `tasksMap` state not triggering a rerender of the component so the updated state is rendered? I only see `setTasksMap` called to update the `tasksMap` within the `useEffect` hook when the `projectId` value updates. – Drew Reese Aug 03 '22 at 21:55
  • So, in the UI there is a text field and a button for adding tasks and the tasks appear on cards with their name and buttons with mark complete and delete (I haven't written the logic for the buttons yet) but when the user clicks the add task button they have to refresh the page to see the new task that was added as the code sits right now. If I try to add the tasksMap to the dependency array of the useEffect, then I'll get the UI to update showing the new project added, but in turn I get an infinite loop. That's why tasksMap is not in the dependency array as the code sits. – Rocket455 Aug 03 '22 at 23:02
  • Sorry, I meant for you to show us the code that is doing this. Is there some other code in your app that is updating this `tasksmap` state? How are tasks added? So far this is an unreproducible code example. See [mcve]. So far my guess is either (a) the code is mutating the state object, or (b) the code is updating something else somewhere else and the code you've shared hasn't any idea about it. It seems you are using a Firebase collection and only getting the docs once. Perhaps you should switch to a subscription/onsnapshot. See [/a/54480212/8690857](/a/54480212/8690857). – Drew Reese Aug 03 '22 at 23:11
  • Oh! Okay. There is no other code that is updating the taskMap's data. I call the function getProjectTasks inside of the useEffect and then call setTasksMap(taskMap). I've uploaded the code onto Github if you would like to take a look. https://github.com/Rocket1969/auto-project-planner A subscription/onsnapshot could actually work. I didn't think of that. – Rocket455 Aug 04 '22 at 01:47
  • Yes, it's effectively the difference between pushing updates and polling for them. The alternative to the onSnapshot could be to add some trigger "refetch" state that triggers the `useEffect` hook callback and conditionally gets the docs/etc somehow, and the callback resets/clears the trigger. When a user clicks the card or whatever to add a task, it somehow updates the "refetch" true so the frontend knows to repull data. – Drew Reese Aug 04 '22 at 01:51
  • I'm going to do some reading on OnShapshot and see if I can't figure out how to impliment that. If I can't figure it out, would you be willing to help me? Thanks for all of your help so far. – Rocket455 Aug 04 '22 at 02:17
  • Sure, feel free to @ me here in a comment. – Drew Reese Aug 04 '22 at 02:19
  • 1
    @DrewReese I got it figured out. I wrapped the onSnapshot listener in a function called memoizedCallback that uses the useCallback hook then I call the memoizedCallback function in useEffect with memoizedCallback as a dependency. I'm still wrapping my head around how exactly this works. I stumbled upon useCallback as ESLint suggested using it. – Rocket455 Aug 05 '22 at 00:59

1 Answers1

1

The useEffect appears to be setting tasksMap when executed. Because this state is an object its reference will change everytime, which will produce an infinite loop

  • Okay. That makes sense. How would you go about fixing this? – Rocket455 Aug 03 '22 at 20:48
  • Why do you want to add `tasksMap` to the dependency array for this effect? – dylangrandmont Aug 03 '22 at 20:57
  • My logic is with the tasksMap added to the dependency array, the user will be kept up to date when they add a task or edit/delete a task such as marking it as complete. Without the taskMap, the user has to refresh to see the updated information. – Rocket455 Aug 03 '22 at 21:01