Search code examples
javascriptreactjstypescriptuse-effect

React - Cleanest way to re-render component on array update


I'm new using react hooks and i was struggling with the useEffect hook due to the following:

I have a simple comment board to post comments and below a list of the comments posted, and i want every time a new comment is made the component reloads to show it.

enter image description here

These commments are got it from an API in the useEffect first parameter, the problem is the second parameter (dependecies array), if i don't put this parameter causes an infinite loop, if i put an empty array it avoids the infinite loop but instead my component doesn't render new comments posted, and if i put the variable state "comments" causes an infinite loop too. So navigating in the internet i found "the solution" that is simply as add another variable state to pass it to the dependencies array and is modified when i call the function "makecomponent" and this way it refreshs the state forcing my component to re render:

export const ForumApp = () => {
    const [comment, setComment] = useState("")
    const [comments, setComments] = useState([])
    const [count, setCount] = useState(0)
 
    useEffect(() => {
        const getComments = async () => {
            try {
                const query = await axios.get("http://127.0.0.1:8000/comments")
                setComments(query.data.comments)
            } catch (error) {
                console.log(error);
            }
        }
        getComments()
    }, [count])

    const handleEnter = (e: KeyboardEvent) => {
        if (e.key === ENTER && !e.shiftKey) {
            e.preventDefault();
            makeComment();
        }
    };

    const makeComment = async () => {
        try {
            await axios.post("http://127.0.0.1:8000/comments/create", {
                time: "1:29",
                content: comment,
            })

            alert("Comment posted!")
            setCount((prevCount) => prevCount + 1)
            setComment("")
        } catch (error) {
            console.log(error)
        }
    };

    return (
        <div className="forum-app">
            <div className="comment-board">
                <h1>Post your comment</h1>
                <textarea
                    placeholder="Write something to us"
                    onChange={(e) => setComment(e.target.value)}
                    onKeyDown={handleEnter}
                    value={comment}
                ></textarea>
                <br />
                <button onClick={makeComment}>Comment</button>
            </div>
            <ol>
                {
                    comments.map(({_id, content}) => (
                        <CommentItem key={_id} content={content}/>
                    ))
                }
            </ol>
        </div>
    )
}

So basically my question is: is this the "correct" way to do it or does exist another cleaner way without declare another variable and only doing it with what i have?


Solution

  • The other cleaner way is likely to use redux & redux-thunk to dispatch an action to do the asynchronous fetching. When the data is fetched and the redux store is updated then the connected components rerender with the latest state from the store.

    This is a lot of new moving parts to add to an app if it isn't already using redux. The important aspect here is the necessary additional "flag" to trigger another data fetch.

    I think a tweak I'd recommend for your code to make it a bit more straight forward is to actually define a fetchData state that is toggled on and off depending on the status of fetching data. This allows you to also display any "loading" state while the data is being fetched.

    export const ForumApp = () => {
        const [comment, setComment] = useState("")
        const [comments, setComments] = useState([])
        const [fetchComments, setFetchComments] = useState(true) // <-- for initial mount call
     
        useEffect(() => {
            const getComments = async () => {
                try {
                    const query = await axios.get("http://127.0.0.1:8000/comments")
                    setComments(query.data.comments)
                } catch (error) {
                    console.log(error);
                } finally {
                  setFetchComments(false); // <-- signify fetching complete
                }
            }
    
            fetchComments && getComments(); // <-- fetch comments if true
        }, [fetchComments])
    
        const handleEnter = (e: KeyboardEvent) => {
            if (e.key === ENTER && !e.shiftKey) {
                e.preventDefault();
                makeComment();
            }
        };
    
        const makeComment = async () => {
            try {
                await axios.post("http://127.0.0.1:8000/comments/create", {
                    time: "1:29",
                    content: comment,
                })
    
                alert("Comment posted!")
                setFetchComments(true); // <-- signify start fetching
                setComment("")
            } catch (error) {
                console.log(error)
            }
        };
    
        return (
            <div className="forum-app">
                <div className="comment-board">
                    <h1>Post your comment</h1>
                    <textarea
                        placeholder="Write something to us"
                        onChange={(e) => setComment(e.target.value)}
                        onKeyDown={handleEnter}
                        value={comment}
                    ></textarea>
                    <br />
                    <button onClick={makeComment}>Comment</button>
                </div>
                <ol>
                    {
                        fetchComments 
                          ? "...fetching comments" // <-- loading state
                          : comments.map(({_id, content}) => (
                            <CommentItem key={_id} content={content}/>
                        ))
                    }
                </ol>
            </div>
        )
    }