ReactJS - UseEffect used with context API is causing infinite loop when dependency array is not kept empty

Viewed 49

devs. so I'm pretty new to React and was trying to implement a simple blog app using ReactJS.

I'm having a problem with my useEffect hook.

import { useContext, useEffect } from "react";
import BlogContext from "../../store/blogs-context";

const BlogList = () => {
    const blogCtx = useContext(BlogContext);

    useEffect(() => {
        blogCtx.getBlogs();
    }, []);

    console.log(blogCtx);

    return <div></div>;
};

export default BlogList;

In the above code when I keep the dependency array empty it works but it keeps giving me a warning. but if I add blogCtx in the dependency array useEffect keeps running infinitely. I believe it's happening because when getBlog() is called blogCtx is updated, and that causes the component to render again which invokes useEffect again and the same cycle. but I can't seem to find any solution :(
I feel kinda stuck here any help would be appreciated.

getBlog() -> makes a GET request to firebase and gets all the available blogs and updates the blogCtx with the retrieved array of blogs.



Code for `Provider` as requested:

import { useReducer } from "react";
import { getBlogs, createBlog } from "../api/blog-api";
import BlogContext from "./blogs-context";

const defaultState = { blog: [] };

const blogsReducer = (state, action) => {
    if (action.type === "FETCH") {
        console.log("hi");
        return action.data;
    }
    if (action.type === "CREATE") {
        console.log(action.data);
        return [...state, action.data];
    }

    return defaultState;
};

const BlogContextProvider = (props) => {
    const [blogList, dispatchBlogAction] = useReducer(
        blogsReducer,
        defaultState
    );

    async function fetchBlog() {
        let blogs = await getBlogs();
        dispatchBlogAction({ type: "FETCH", data: blogs });
    }

    async function createNewBlog(blogData) {
        let response = await createBlog(blogData);
        let responseBody = await response.json();
        const newBlog = { ...blogData, id: responseBody.name };
        if (response.status === 200)
            dispatchBlogAction({ type: "CREATE", data: newBlog });

        return response;
    }

    const blogCtx = {
        blogs: blogList,
        getBlogs: fetchBlog,
        addBlog: createNewBlog,
    };

    return (
        <BlogContext.Provider value={blogCtx}>
            {props.children}
        </BlogContext.Provider>
    );
};

export default BlogContextProvider;

Edit: spelling, added code for provider file as requested.

1 Answers

The reason this happens is as follows:

  1. The effect runs on mount and calls getBlogs.
  2. getBlogs updates the blogList state, which causes the provider to rerender.
  3. When the provider rerenders, the context object is recreated because it is not memoized.
  4. Now the effect runs again since blogCtx.getBlogs function was recreated on the previous render.
  5. Back to 1

This fix for this is memoization (https://en.wikipedia.org/wiki/Memoization). React has built in hooks useMemo and useCallback (basically useMemo but for functions) that make it so the contents are not recalculated on render unless the deps change. This means between renders, the resulting variables will be the exact same object, rather than a new object that is basically a clone of the previous one. Since useEffect doesn't do any clever diffing, without memoization it thinks that the context has changed on every render since it instantiated a new object.

You need to useMemo the blogCtx in the provider such that it is stable and doesn't get recreated each render. You will also need to memoize the functions contained within with useCallback so that they don't continually cause the memo to recalculate.

This means the context, and functions contained within, will be referentially stable between renders and will no longer trigger your effect constantly.

import { useReducer, useMemo, useCallback } from "react";
import { getBlogs, createBlog } from "../api/blog-api";
import BlogContext from "./blogs-context";

const defaultState = { blog: [] };

const blogsReducer = (state, action) => {
    if (action.type === "FETCH") {
        console.log("hi");
        return action.data;
    }
    if (action.type === "CREATE") {
        console.log(action.data);
        return [...state, action.data];
    }

    return defaultState;
};


const BlogContextProvider = (props) => {
    const [blogList, dispatchBlogAction] = useReducer(
        blogsReducer,
        defaultState
    );

    const fetchBlog = useCallback(async () => {
        let blogs = await getBlogs();
        dispatchBlogAction({ type: "FETCH", data: blogs });
    }, [getBlogs])

    const createNewBlog = useCallback(async (blogData) => {
        let response = await createBlog(blogData);
        let responseBody = await response.json();
        const newBlog = { ...blogData, id: responseBody.name };
        if (response.status === 200)
            dispatchBlogAction({ type: "CREATE", data: newBlog });

        return response;
    }, [createBlog])

    const blogCtx = useMemo(() => ({
        blogs: blogList,
        getBlogs: fetchBlog,
        addBlog: createNewBlog,
    }), [blogList, fetchBlog, createNewBlog]);

    return (
        <BlogContext.Provider value={blogCtx}>
            {props.children}
        </BlogContext.Provider>
    );
};

export default BlogContextProvider;


  [1]: https://en.wikipedia.org/wiki/Memoization
Related