Getting this error when deleting the comment: Can't perform a React state update on an unmounted component

I’m getting this error message when I reply to the comment or delete a comment.

index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
    in Comment (at SolutionComments.js:23)

I am not using useEffect hook, still, it’s saying “cancel all subscriptions and asynchronous tasks in a useEffect cleanup function:”. Anyone, please help me with this error!

SolutionComments.js

import React, { useState } from "react"
import { useParams } from "react-router-dom"

import { useCollection } from "../../hooks/useCollection"

import Comment from "./Comment"
import CommentForm from "./CommentForm"

const SolutionComments = () => {
  const [activeComment, setActiveComment] = useState(null)
  const { id } = useParams()
  const { documents } = useCollection(`solutions/${id}/comments`, null, 4, [
    "createdAt",
    "desc",
  ])

  return (
    <div className="mt-10">
      <CommentForm docID={id} />
      <div>
        {documents &&
          documents.map((comment) => (
            <Comment // Line number 23
              key={comment.id}
              comment={comment}
              replies={comment.replies}
              activeComment={activeComment}
              setActiveComment={setActiveComment}
            />
          ))}
      </div>
    </div>
  )
}

export default SolutionComments

Comment.js

import React from "react"
import moment from "moment"
import { useParams } from "react-router-dom"

import { useAuthContext } from "../../hooks/useAuthContext"
import { useFirestore } from "../../hooks/useFirestore"

import CommentReply from "./CommentReply"
import ReplyForm from "./ReplyForm"

const Comment = ({
  comment,
  replies,
  activeComment,
  setActiveComment,
  parentId = null,
}) => {
  const { deleteSubCollectionDocument } = useFirestore("solutions")
  const { id: docID } = useParams()
  const { user } = useAuthContext()

  const isEditing =
    activeComment && activeComment.id === comment.id && activeComment.type === "editing"
  const isReplying =
    activeComment && activeComment.id === comment.id && activeComment.type === "replying"
  const replyId = parentId || comment.id

  // handle sub collection document
  const handleDelete = async () => {
    if (window.confirm("Do you really want to delete this comment?")) {
      await deleteSubCollectionDocument(docID, comment.id)
    }
  }

  return (
    <div className="my-4 border border-gray-800 rounded p-4">
      <div className="flex">
        <a
          href={`https://github.com/${comment.user.username}`}
          target="_blank"
          rel="noopener noreferrer"
        >
          <img
            className="w-12 rounded-full border-2 border-gray-800"
            src={comment.user.avatarURL}
            alt="avatar"
          />
        </a>
        <div className="ml-4 flex-1">
          <p className="text-gray-300 mb-2">
            <a
              href={`https://github.com/${comment.user.username}`}
              target="_blank"
              rel="noopener noreferrer"
            >
              {comment.user.displayName
                ? comment.user.displayName
                : comment.user.username}
            </a>
            <small className="pl-2 text-gray-400">
              {moment(comment.createdAt.toDate()).fromNow()}
            </small>
          </p>
          <div className="mt-2 flex">
            {user && (
              <button
                onClick={() => setActiveComment({ id: comment.id, type: "replying" })}
                className="text-gray-400"
              >
                <i className="fas fa-reply"></i>
                <small className="pl-2 font-semibold">Reply</small>
              </button>
            )}
            {user?.uid === comment.user.userID && (
              <>
                <button className="text-gray-400" onClick={handleDelete}>
                  <i className="fas fa-trash-alt"></i>
                  <small className="pl-2 font-semibold">Delete</small>
                </button>
              </>
            )}
          </div>
          {isReplying && (
            <ReplyForm
              docID={docID}
              replyingTo={comment.user.username}
              id={replyId}
              replies={replies}
              hasCancelButton
              setActiveComment={setActiveComment}
            />
          )}
          {replies &&
            replies
              .sort((a, b) => a.createdAt.seconds - b.createdAt.seconds)
              .map((reply) => (
                <CommentReply
                  key={reply.id}
                  comment={reply}
                  parentReplies={replies}
                  parentId={comment.id}
                  activeComment={activeComment}
                  setActiveComment={setActiveComment}
                />
              ))}
        </div>
      </div>
    </div>
  )
}

export default Comment

useFirestore hook

import { useEffect, useReducer, useState } from "react"
import {
  addDoc,
  collection,
  deleteDoc,
  doc,
  serverTimestamp,
  updateDoc,
} from "firebase/firestore"

import { db } from "../firebase/config"

const initialState = {
  document: null,
  isPending: false,
  error: null,
  success: null,
}

const firestoreReducer = (state, action) => {
  switch (action.type) {
    case "IS_PENDING":
      return { isPending: true, document: null, success: false, error: null }
    case "ADDED_DOCUMENT":
      return { isPending: false, document: action.payload, success: true, error: null }
    case "DELETED_DOCUMENT":
      return { isPending: false, document: null, success: true, error: null }
    case "UPDATED_DOCUMENT":
      return { isPending: false, document: action.payload, success: true, error: null }
    case "ERROR":
      return { isPending: false, document: null, success: false, error: action.payload }
    default:
      return state
  }
}

export const useFirestore = (c) => {
  const [response, dispatch] = useReducer(firestoreReducer, initialState)
  const [isCancelled, setIsCancelled] = useState(false)

  // only dispatch is not cancelled
  const dispatchIfNotCancelled = (action) => {
    if (!isCancelled) {
      dispatch(action)
    }
  }

  // add a document
  const addDocument = async (doc) => {
    dispatch({ type: "IS_PENDING" })

    try {
      const createdAt = serverTimestamp()
      const addedDocument = await addDoc(collection(db, c), {
        ...doc,
        createdAt,
      })
      dispatchIfNotCancelled({ type: "ADDED_DOCUMENT", payload: addedDocument })
    } catch (error) {
      dispatchIfNotCancelled({ type: "ERROR", payload: error.message })
    }
  }

  const updateSubCollectionDocument = async (docID, id, updates) => {
    dispatch({ type: "IS_PENDING" })
    try {
      const updatedDocument = await updateDoc(doc(db, c, docID, "comments", id), updates)
      dispatchIfNotCancelled({ type: "UPDATED_DOCUMENT", payload: updatedDocument })
      return updatedDocument
    } catch (error) {
      console.log(error)
      dispatchIfNotCancelled({ type: "ERROR", payload: error })
      return null
    }
  }

  const deleteSubCollectionDocument = async (docID, id) => {
    dispatch({ type: "IS_PENDING" })

    try {
      await deleteDoc(doc(db, c, docID, "comments", id))
      dispatchIfNotCancelled({ type: "DELETED_DOCUMENT" })
    } catch (error) {
      dispatchIfNotCancelled({ type: "ERROR", payload: error })
    }
  }

  useEffect(() => {
    return () => setIsCancelled(true)
  }, [isCancelled])

  return {
    addDocument,
    updateSubCollectionDocument,
    deleteSubCollectionDocument,
    response,
  }
}

useCollection hook:

import { useEffect, useRef, useState } from "react"
// firebase import
import { collection, limit, onSnapshot, orderBy, query, where } from "firebase/firestore"

import { db } from "../firebase/config"

export const useCollection = (c, _q, _l, _o) => {
  const [documents, setDocuments] = useState([])
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState(null)

  // if we don't use a ref --> infinite loop in useEffect
  // _query is an array and is "different" on every function call
  const q = useRef(_q).current
  const o = useRef(_o).current

  useEffect(() => {
    let ref = collection(db, c)

    if (q) {
      ref = query(ref, where(...q))
    }
    if (o) {
      ref = query(ref, orderBy(...o))
    }
    if (_l) {
      ref = query(ref, limit(_l))
    }

    const unsubscribe = onSnapshot(ref, (snapshot) => {
      const results = []
      snapshot.docs.forEach(
        (doc) => {
          results.push({ ...doc.data(), id: doc.id })
        },
        (error) => {
          console.log(error)
          setError("could not fetch the data")
        }
      )
      // update state
      setDocuments(results)
      setIsLoading(false)
      setError(null)
    })

    // unsubscribe on unmount
    return () => unsubscribe()
  }, [c, q, _l, o, isLoading])

  return { documents, error, isLoading }
}

hello and welcome back :slight_smile:

it’s unclear how to trace back to this error from your given code, without actually seeing it ‘live’!! perhaps others can be more useful!!

Hi @rishipurwar007

Well, the error message looks quite clear. With the several conditional rendering you are doing, most likely you are updating state after unmounting a component. That is why React recommends performing side effects inside a useEffect hook. You can cancel any pending side effect when the component unmounts.

Can you please tell me in which component should I look?

@nibble On thing I found when I remove this useFirestore call const { deleteSubCollectionDocument } = useFirestore("solutions") from Comment.js and put it to the parent component that’s SolutionComments.js, error went way. I passed it as a prop to the Comment.js from SolutionComment.js.
Do you have any idea why is it happenning?

Hi @rishipurwar007

I don’t think I can answer that question unless I play with your code to make sense of how it works. In your original post, you said the warning occurs when you delete a comment or reply to a comment. I noticed when you click the delete button, it triggers the handleDelete event handler.

Therefore, it is important to understand what happens in the process of updating firestore. Is there a component which you are unmounting while updating the DB? If so, most likely it is the offending state update React is warning you about. I am certain there is a component which you are intentionally or unintentionally updating after it is unmounted. I hope this helps you investigate the problem a little further and pinpoint the exact cause of the React warning.