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 }
}