Search code examples
reactjsfirebasefirebase-realtime-databaseuse-effect

I am trying to figure out how to create a clean up function as I keep getting an error


I am trying to figure out how to create a clean up function as I keep getting an error, if I remove "comments" from the useEffect dependencies, the error goes away, but then the app doesn't update in realtime, which is a problem. If anyone has worked with React and the realtime database or even Firestore and have any ideas on what I should do please let me know.

import React, { useContext, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'react-toastify';

import User from '../assets/images/user.svg';

import { AuthContext } from '../helpers/firebaseAuth';
import firebase from '../helpers/Firebase';
import Loading from '../helpers/Loading';

export const Comments = ({ match, history }) => {
    const { register, handleSubmit, reset } = useForm();

    const slug = match.params.slug;

    const {...currentUser} = useContext(AuthContext);

    const [comments, setComments] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {

        const fetchData = () => {
            const data = firebase.database().ref(`/posts/${slug}/comments`)

            data.once('value')
            .then((snapshot) => {
                if (snapshot) {
                    let comments = [];
                    const snapshotVal = snapshot.val();
                    for (let comment in snapshotVal) {
                        comments.push(snapshotVal[comment]);
                    }
                    setComments(comments);
                    setLoading(false);
                }
            });
        }
        fetchData();
    }, [slug, comments])


    if (loading) {
        return <Loading />;
    };

    const postComment = (values) => {

        console.log(!!currentUser.currentUser)

        if (!!currentUser.currentUser) {
            const comment = {
                commentText: values.commentText,
                commentCreator: currentUser.currentUser.displayName,
                currentUserId: currentUser.currentUser.uid,
            }

            const postRef = firebase.database().ref(`posts/${slug}/comments`);
            postRef.push(comment);

            reset();
        } else {
            toast.error('You are not authenticated 😕');
        }
    };

    const deleteComment = () => {

        console.log(comments[0].commentUserId);
        console.log(currentUser.currentUser.uid);

        if (currentUser.currentUser.uid === comments[0].commentUserId) {
            console.log('correct');
        }

        const key = firebase.database().ref(`posts/${slug}/comments`).once('value');

        key.then(snapshot => {
            console.log(snapshot.val());
        }).catch((error) => {
            console.log(error);
        });

    };

    const back = () => {
        history.push('./');
    };

    return (
        <div className='main' style={{ maxWidth: '600px' }}>
            <div className='see-user-comments' onClick={back} style={{ cursor: 'pointer', height: '50px' }}>
                Commenting on the post: {slug}
            </div>
            <div className='see-user-comments' style={{ padding: '10px 0' }}>
                <div>
                    <img src={User} alt='Profile' style={{ width: '30px' }} />
                    <span className='usertag-span'>{currentUser.displayName}</span>
                </div>
                <div>
                    <form onSubmit={handleSubmit(postComment)}>
                        <textarea 
                            name='commentText'
                            rows='3'
                            style={{ margin: '10px 0' }}
                            placeholder='Add to the conversation!'
                            ref={register} 
                        /> 
                        <span style={{ width: '90%' }}>
                            <button>Comment</button>
                        </span>
                    </form>
                </div>
            </div>
            {comments.map((comment, index) =>
            <div key={index} className='see-user-comments' style={{ padding: '15px 0' }}>
                <div style={{ height: '30px' }}>
                    <img src={User} alt='Profile' style={{ width: '30px' }} />
                    <div style={{ flexDirection: 'column', alignItems: 'flex-start', justifyItems: 'center' }}>
                        <span className='usertag-span'>{comment.commentCreator}</span>
                    </div>
                </div>
                <span className='commentText-span'>{comment.commentText}
                    { !!currentUser?.currentUser?.uid === comments[0].commentUserId ?
                        (<button onClick={deleteComment}>Delete</button>) : null 
                    }
                </span>
            </div>
            )}
        </div>
    )
}

export default Comments;

Solution

  • Without seeing the error in question, I can only assume it's because using the following pattern causes an infinite loop because the effect is re-triggered every time count changes:

    const [count, setCount] = useState(0);
    useEffect(() => setCount(count + 1), [count]);
    

    When you add comments to your effect, you are doing the same thing.

    To solve this, you must change your effect to rely on Firebase's realtime events to update your comments array instead. This can be as simple as changing once('value').then((snap) => {...}) to on('value', (snap) => {...});. Because this is now a realtime listener, you must also return a function that unsubscribes the listener from inside your useEffect call. The least amount of code to do this correctly is:

    const [postId, setPostId] = useState('post001');
    
    useEffect(() => {
        const postRef = firebase.database().ref('posts').child(postId);
        const listener = postRef.on(
            'value',
            postSnapshot => {
                const postData = postSnapshot.val();
                // ... update UI ...
            },
            err => {
                console.log('Failed to get post', err);
                // ... update UI ...
            }
        )
    
        return () => postRef.off('value', listener);
    
    }, [postId]);
    

    Applying these changes to your code (as well as some QoL improvements) yields:

    const { register, handleSubmit, reset } = useForm();
    
    const slug = match.params.slug;
    
    const { ...authContext } = useContext(AuthContext); // renamed: currentUser -> authContext (misleading & ambiguous)
    
    const [comments, setComments] = useState([]);
    const [loading, setLoading] = useState(true);
    
    let _postCommentHandler, _deleteCommentHandler;
    
    useEffect(() => {
        // don't call this data - it's not the data but a reference to it - always call it `somethingRef` instead
        const postCommentsRef = firebase.database().ref(`/posts/${slug}/comments`);
    
        // create realtime listener
        const listener = postCommentsRef.on(
            'value',
            querySnapshot => {
                let _comments = [];
                querySnapshot.forEach(commentSnapshot => {
                    const thisComment = commentSnapshot.val();
                    thisComment.key = commentSnapshot.key; // store the key for delete/edit operations
                    _comments.push(thisComment);
                });
                setComments(_comments);
                setLoading(false);
            },
            err => {
                console.log(`Error whilst getting comments for post #${slug}`, err);
                // TODO: handle error
            });
    
        // update new comment handler
        _postCommentHandler = (formData) => {
            console.log({
                isLoggedIn: !!authContext.currentUser
            });
    
            if (!authContext.currentUser) {
                toast.error('You are not authenticated 😕');
                return;
            }
    
            const newComment = {
                commentText: formData.commentText, // suggested: commentText -> content
                commentCreator: authContext.currentUser.displayName, // suggested: commentCreator -> author
                currentUserId: authContext.currentUser.uid, // suggested: commentUserId -> authorId
            }
    
            postCommentsRef.push(newComment)
                .then(() => {
                    // commented successfully
                    reset(); // reset form completely
                })
                .catch(err => {
                    console.log(`Error whilst posting new comment`, err);
                    // TODO: handle error
                    reset({ commentText: formData.commentText }) // reset form, but leave comment as-is
                })
        }
    
        // update delete handler
        _deleteCommentHandler = () => {
            if (!comments || !comments[0]) {
                console.log('Nothing to delete');
                return;
            }
    
            const commentToDelete = comments[0];
    
            console.log({
                commentUserId: commentToDelete.commentUserId,
                currentUser: authContext.currentUser.uid
            });
    
            if (authContext.currentUser.uid !== commentToDelete.commentUserId) {
                toast.error('That\'s not your comment to delete!');
                return;
            }
    
            postCommentsRef.child(commentToDelete.key)
                .remove()
                .then(() => {
                    // deleted successfully
                })
                .catch(err => {
                    console.log(`Error whilst deleting comment #${commentToDelete.key}`, err);
                    // TODO: handle error
                });
        };
    
        // return listener cleanup function
        return () => postCommentsRef.off('value', listener);
    }, [slug]);
    
    const postComment = (values) => _postCommentHandler(values);
    const deleteComment = () => _deleteCommentHandler();
    

    Because I renamed currentUser to authContext, this will also need updating:

    <div>
        <img src={User} alt='Profile' style={{ width: '30px' }} />
        <span className='usertag-span'>{authContext?.currentUser?.displayName}</span>
    </div>