skip to Main Content

I’m making a todo app using react and firebase. I followed a tutorial from youtube to write the code.

Now I’m trying to add firebase. I’m using a useEffect hook to fetch data from firebase. It works on first render, but if I use other functionality like read, update it doesn’t re-render. On refresh I get the result I wanted. If I add todos as a dependency to the useEffect, it works correctly but their are too many api calls.

App.jsx

function App() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState("");
  console.log(input);

  // Create todo
  const createTodo = async (e) => {
    e.preventDefault(e);
    if (input === "") {
      alert("Please enter input");
      return;
    }
    await addDoc(collection(db, "react-todo"), {
      text: input,
      complete: false,
    });
    setInput("");
  };

  // Read todo from firebase
  useEffect(() => {
    const getTodos = async () => {
      const data = await getDocs(collection(db, "react-todo"));
      let todoArray = [];
      data.forEach((doc) => {
        todoArray.push({ ...doc.data(), id: doc.id });
      });
      setTodos(todoArray);
    };
    getTodos();
  }, []);

  // update todo in firebase
  const toggleComplete = async (todo) => {
    await updateDoc(doc(db, "react-todo", todo.id), {
      complete: !todo.complete,
    });
  };

  // delete todo
  const deleteTodo = async (todo) => {
    await deleteDoc(doc(db, "react-todo", todo.id));
  };

  return (
    <div className={style.bg}>
      <div className={style.container}>
        <h3 className={style.heading}>Todo App</h3>
        <form onSubmit={createTodo} className={style.form}>
          <input
            type="text"
            placeholder="Add Todo"
            className={style.input}
            value={input}
            onChangeCapture={(e) => setInput(e.target.value)}
          />
          <button className={style.button}>
            <AiOutlinePlus size={30} />
          </button>
        </form>
        <ul>
          {todos.map((todo, index) => (
            <Todo
              key={index}
              todo={todo}
              toggleComplete={toggleComplete}
              deleteTodo={deleteTodo}
            />
          ))}
        </ul>
        <p className={style.count}>{`You have ${todos.length} no. of todos`}</p>
      </div>
    </div>
  );
}

Todo.jsx

function Todo({ todo, toggleComplete, deleteTodo }) {
  return (
    <li className={todo.complete ? style.liComplete : style.li}>
      <div className={style.row}>
        <input
          onChange={() => toggleComplete(todo)}
          type="checkbox"
          checked={todo.complete ? "checked" : ""}
        />
        <p
          onClick={() => toggleComplete(todo)}
          className={todo.complete ? style.textComplete : style.text}
        >
          {todo.text}
        </p>
      </div>
      <button onClick={() => deleteTodo(todo)}>{<FaRegTrashAlt />}</button>
    </li>
  );
}

2

Answers


  1. If you want to avoid an API call on every action so you should update your todos locally manipulating data you’ve changed:

    const createTodo = async (newTodo) => {
      ... handle db logic
        
      setTodos((current) => ([
        ...current,
        newTodo
      ]))
    };
    
    const toggleComplete = async (todo) => {
      ... handle your db logic
    
      setTodos((currents) => currents.map(current => {
        if(current.id !== todo.id) return current
    
        return {
          ...todo,
          complete: !todo.complete
        }
      }))
    };
    
    const deleteTodo = async (todo) => {
      .... handle your db logic
    
      setTodos((currents) => currents.filter(current => current.id !== todo.id))
    };
    

    Only make sure your data was saved successfully and now you can just update what user see locally. Hope this helps

    Login or Signup to reply.
  2. Adding todos to your dependency array will create an infinite loop and is not the way to do it. As you said "there are too many api calls. This is because setTodos creates a new array each time it’s called, which then causes the useEffect to run…which then calls setTodos, and so on.

    Even though you call getTodos on first mount, react doesn’t have any way of knowing that your database has updated when you call functions like deleteTodo. You need to manually refetch the data after each call. Something like this should work:

    App.jsx

    function App() {
      const [todos, setTodos] = useState([]);
      const [input, setInput] = useState("");
    
      // Get todos
      const getTodos = async () => {
        const data = await getDocs(collection(db, "react-todo"));
        let todoArray = [];
        data.forEach((doc) => {
          todoArray.push({ ...doc.data(), id: doc.id });
        });
        setTodos(todoArray);
      };
    
      // Create todo
      const createTodo = async (e) => {
        e.preventDefault(e);
        if (input === "") {
          alert("Please enter input");
          return;
        }
        await addDoc(collection(db, "react-todo"), {
          text: input,
          complete: false,
        });
        setInput("");
        getTodos(); // REFETCH
      };
    
      // Read todo from firebase
      useEffect(() => {
        getTodos(); // Initial fetch on first mount
      }, []);
    
      // update todo in firebase
      const toggleComplete = async (todo) => {
        await updateDoc(doc(db, "react-todo", todo.id), {
          complete: !todo.complete,
        });
        getTodos(); // REFETCH
      };
    
      // delete todo
      const deleteTodo = async (todo) => {
        await deleteDoc(doc(db, "react-todo", todo.id));
        getTodos(); // REFETCH
      };
    
      return (
        <div className={style.bg}>
          <div className={style.container}>
            <h3 className={style.heading}>Todo App</h3>
            <form onSubmit={createTodo} className={style.form}>
              <input
                type="text"
                placeholder="Add Todo"
                className={style.input}
                value={input}
                onChangeCapture={(e) => setInput(e.target.value)}
              />
              <button className={style.button}>
                <AiOutlinePlus size={30} />
              </button>
            </form>
            <ul>
              {todos.map((todo, index) => (
                <Todo
                  key={index}
                  todo={todo}
                  toggleComplete={toggleComplete}
                  deleteTodo={deleteTodo}
                />
              ))}
            </ul>
            <p className={style.count}>{`You have ${todos.length} no. of todos`}</p>
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search