1

I wanna load the first batch of comments immediately (using useEffect) and then load additional pages when a "load more" button is pressed.

The problem is that my current setup causes an infinite loop (caused by the dependency on comments).

If I remove the fetchNextCommentsPage function from the useEffect dependency list, everything seems to work, but EsLint complains about the missing dependency.

    const [comments, setComments] = useState<CommentModel[]>([]);
    const [commentsLoading, setCommentsLoading] = useState(true);
    const [commentsLoadingError, setCommentsLoadingError] = useState(false);

    const [paginationEnd, setPaginationEnd] = useState(false);

    const fetchNextCommentsPage = useCallback(async function () {
        try {
            setCommentsLoading(true);
            setCommentsLoadingError(false);
            const continueAfterId = comments[comments.length - 1]?._id;
            const response = await BlogApi.getCommentsForBlogPost(blogPostId, continueAfterId);
            setComments([...comments, ...response.comments]);
            setPaginationEnd(response.paginationEnd);
        } catch (error) {
            console.error(error);
            setCommentsLoadingError(true);
        } finally {
            setCommentsLoading(false);
        }
    }, [blogPostId, comments])

    useEffect(() => {
        fetchNextCommentsPage();
    }, [fetchNextCommentsPage]);
2
  • have you tried not wrapping up the fetcher in useCallback?. Commented Feb 1, 2023 at 9:38
  • @aleEspinosaM Yes, that's what I started out with. Then EsLint lights up the whole function and warns that this will cause the useEffect to run on every render. Commented Feb 1, 2023 at 9:59

2 Answers 2

2

Never put the state you want to mutate in the dependencies list as it will always raise an infinite loop issue. The common way to solve this is to use the callback function of setState https://reactjs.org/docs/react-component.html#setstate.

If you want your effect triggered only once, put something that never changes after the first loading. When you want to load more when pressing a button, just change the dependencies of your effect to run your effect again with new dependency value.

    const [comments, setComments] = useState<CommentModel[]>([]);
    const [commentsLoading, setCommentsLoading] = useState(true);
    const [commentsLoadingError, setCommentsLoadingError] = useState(false);
    const [continueAfterId, setContinueAfterId] = useState(null)

    const [paginationEnd, setPaginationEnd] = useState(false);

    const fetchNextCommentsPage = useCallback(async function () {
        try {
            setCommentsLoading(true);
            setCommentsLoadingError(false);
            const response = await BlogApi.getCommentsForBlogPost(blogPostId, continueAfterId);
            setComments(previousState => [...previousState, ...response.comments]);
            setPaginationEnd(response.paginationEnd);
        } catch (error) {
            console.error(error);
            setCommentsLoadingError(true);
        } finally {
            setCommentsLoading(false);
        }
    }, [blogPostId, continueAfterId]);

    useEffect(() => {
        fetchNextCommentsPage();
    }, [fetchNextCommentsPage]);

    const onButtonPressed = useCallback(() => {
        // continueAfterId is one of the dependencies of fetchNextCommentsPage so it will change `fetchNextCommentsPage`, hence trigger the effect
        setContinueAfterId(comments[comments.length - 1]?._id)
    }, [comments])

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you! This trigger the initial load twice. Any idea why? Maybe this only happens in debugging because of React's double render. Edit: I think the double fetch is caused by React's strict mode and should not happen in production.
Also, what do you think about passing continueAfterId as an argument to fetchNextCommentspage and removing it from the dependency array of the useCallback? It feels more intuitive to me than using continueAfterId as a trigger.
@FlorianWalther yes, you can absolutely do that so onButtonPress you will calcuate the next id and call the fetchComments(newId) again. It's an even better solution actually :D. Then you don't need another state for it.
Do you want to edit your solution or should I post my approach myself?
0

Thank you to @Đào-minh-hạt for their answer. I improved it by passing the continueAfterId as an argument, rather than holding it in another state (which, I think, is more intuitive):

const [comments, setComments] = useState<CommentModel[]>([]);
const [commentsLoading, setCommentsLoading] = useState(true);
const [commentsLoadingError, setCommentsLoadingError] = useState(false);

const [paginationEnd, setPaginationEnd] = useState(false);

const fetchNextCommentsPage = useCallback(async function (continueAfterId?: string) {
    try {
        setCommentsLoading(true);
        setCommentsLoadingError(false);
        const response = await BlogApi.getCommentsForBlogPost(blogPostId, continueAfterId);
        setComments(existingComments => [...existingComments, ...response.comments]);
        setPaginationEnd(response.paginationEnd);
    } catch (error) {
        console.error(error);
        setCommentsLoadingError(true);
    } finally {
        setCommentsLoading(false);
    }
}, [blogPostId]);

useEffect(() => {
    fetchNextCommentsPage();
}, [fetchNextCommentsPage]);

And then in my load-more button's onClick:

<Button
   variant="outline-primary"
   onClick={() => fetchNextCommentsPage(comments[comments.length - 1]?._id)}>
    Load more comments
</Button>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.