How calling setState in useEffect can cause re-renders in a loop.

Photo by Emre Turkan on Unsplash

How calling setState in useEffect can cause re-renders in a loop.

And using Static Site Generation to improve the SEO and page loading speed.

I was developing a blog site.

And I knew if I used the setState function in useEffect it could cause an error but I did not know how to fix it.

I was fetching data in useEffect to display the page with its respective data and I also used dynamic routes in nextJs to generate different pages for each blog.

Here's the code causing the re-rendering of the page in the loop.

const router = useRouter()
    const blogSlug = router.query.blog

const [blog, setBlog] = useState(dummyBlog)

    useEffect(() => {
        const fetchBlog = async () => {
            const q = query(collection(db, "blogs"), where("slug", "==", blogSlug));

            const querySnapshot = await getDocs(q);
            querySnapshot.forEach((doc) => {
                // doc.data() is never undefined for query doc snapshots
                console.log(doc.id, " => ", doc.data());
                setBlog(doc.data())
            });
        }
        fetchBlog()
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [])

If you see the dependency array is empty, it means this useEffect will run every time the component rendering this page mounts.

When the page loads this useEffect runs, it fetches the blog document from the database and changes the state of the blog to the fetched data.

But, by calling the setBlog function it calls the page to render again which calls the useEffect function, which then again calls the setBlog function and this forms a loop rendering the page again and again.

To solve this issue I added the slug of the page in the dependency array.

const router = useRouter()
    const blogSlug = router.query.blog

useEffect(() => {
        const fetchBlog = async () => {
            const q = query(collection(db, "blogs"), where("slug", "==", blogSlug));

            const querySnapshot = await getDocs(q);
            querySnapshot.forEach((doc) => {
                // doc.data() is never undefined for query doc snapshots
                console.log(doc.id, " => ", doc.data());
                setBlog(doc.data())
            });
        }
        fetchBlog()
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [blogSlug])

Now, the useEffect runs when the blogSlug changes. But why blogSlug?

Because this page is rendered using dynamic routing in nextJs, eg...[blog].jsx and the fetching of blog data every time the blogSlug changes make sure that not every page rendered through [blog].jsx doesn't display the same content and displays the content respective to the blogSlug.

And it also solves re-rendering of the page in a loop at the useEffect doesn't run every time the page loads, but only when the blogSlug changes.

I declared another function inside useEffect to fetch data asynchronously. I don't know why you can't call the anonymous function inside useEffect asynchronously. Please let me know in the comments.

Using Static Site Generation to improve the SEO and page loading speed.

I was thinking about using Sever Side Rendering instead. But that would only fix the SEO problem and may cause some page loading speed issues and would make a database call on every blog page visit.

If you have readers in millions that would increase your read operations on the database and thus the cost of hosting the database.

but if you use Static Site Generation it would only make a database call while running the build script, not every time someone visits that blog page, thus reducing the read operations on the database.

Here's how instead of running useEffect every time loading the page, I simplified it, and improved SEO and page loading speed.

export const getStaticPaths = async () => {

    const querySnapshot = await getDocs(collection(db, "blogs"));

    let blogSlugs = []
    querySnapshot.forEach((doc) => {
        blogSlugs = [...blogSlugs, `/blogs/${doc.data().slug}`]
    });

    return {
        paths: blogSlugs,
        fallback: false
    }
}

export const getStaticProps = async (context) => {

    const blogSlug = context.params.blog
    console.log(blogSlug)

    const q = query(collection(db, "blogs"), where("slug", "==", blogSlug));

    let blog = {}
    const querySnapshot = await getDocs(q);
    querySnapshot.forEach((doc) => {
        blog = doc.data()
    });

    return {
        props: { blog: blog }
    }
}

The getStaticProps function gets the blog slug from the context prop, fetches the data using the slug, and returns the blog data in props to the functional component rendering the page.

But as I was using dynamic routing, I also needed to provide all the paths for which the static pages should be generated.

The getStaticPaths function fetches every blog slug from the database, stores the paths in an array, and returns the paths, Thus nextJs knows how many static pages to generate at what paths.

There's still an issue. If I do a write operation to the database, my nextJs app has no idea about the changes in the database and does not create a page for that new document in the database.

Because SSG only runs when the build script runs. This could be solved through webhooks or in other ways. If you have suggestions to solve this issue, let me know in the comments.

Let's wrap this up.