Rushing Labs

React is weird...and the lesson I learned

It feels like every time I step away from React and come back something has changed. This time it was useEffect.

So, my issue was throwing a pretty pedestrian fetch() inside of useEffect() to automatically load data upon first load for a component. I could've sworn this was a pattern I had used in the past. But somehow now my useEffect logic seems to be executing twice. Am I forgetting something?!


Not exactly as Dan Abramov explained here, and if you want the tl;dr check out this article by Zachary Lee. He explains it well.

Skip to the code


useEffect firing twice produces this result from a double fetch

The Issue§

We'll cover the issue with my code in-depth below, but the short answer is: useEffect() now executes twice because React 18 mounts, unmounts, and remounts components by default, when running in development mode with React.StrictMode enabled. This is to help developers highlight potential issues with component state logic earlier in development.

So, my issue with this isn't that "React changed"...but that this behavior is removed from the context of why it exists. If a possibly unexpected behavior is going to be running by default to help prevent errors, specifically in development under Strict Mode, then shuoldn't a warning also pop by default?

Perhaps I'm blind, or just not smart enough to know where in the docs to start reading at. But this experience with React is something I've experienced several times, and in the past I always just chalked it up to not being in the ecosystem daily or some type of "JavaScript fatigue".

But! but...this felt different. I thought I had legitimately made an error—not, encountered a different behavior of equivalent code. So, instead of immediately going to Read-The-Free-Manual, I started down the road of Googling to find articles, etc. hoping to find where others explained how they fixed this seemingly elementary stumbling block I had forgotten.

Discovering, Getting back to the docs§

No, instead I found a random (but fortunate) blog article that pointed me back to the section of the updated docs, I didn't know was going to be worth reviewing.

Article -> GH issue§

In this article, I see Zachary explaining how he stumbled through the double-call, and where he found Dan Abramov explaining it all in a GitHub issue

Ah. So, we're ensuring reusable state. Well, that's certainly a good thing, but that's a weird place to mention it. 🤷‍♂

GH Issue -> Reddit reply§

Now, reading Dan Abramov's full reply on the GitHub issue, he references a Reddit post.

https://www.reddit.com/r/reactjs/comments/vi6q6f/comment/iddrjue/

The Reddit post is exceptionally thorough, referencing other blog posts, the new React docs beta, and he even provides some explanation for an attempt to consolidate the docs. (Kind of my point. Very thankful it's being addressed! 👍) It's definitely worth a full read if you're working this fetch/useEffect issue.

Article -> React Docs (BETA)§

At the end of Zachary's blog article, and also in Dan's Reddit reply we get references to the new React Docs Beta: https://beta-reactjs-org-git-effects-fbopensource.vercel.app/learn/synchronizing-with-effects

Again...AWESOME! but holy cow we had to dig for this...

The whole Synchronizing with Effects is worth the read. However...

This is the part I needed§

Skipping down to "Step 3: Add cleanup if needed", in this page of the React Docs Beta, they begin walking through the useEffect() issue. It uses a hypothetical ChatRoom application as the backdrop to explain this change surrounding useEffect.

Skip to the Code§

All that to just use the same old fetch() from useEffect() with a flag check?!

Yes. I'm writing a simple app. All I need is a "grab from database" upon component load. It's all local development, so I can handle round trips without a caching layer. And forcing this "flag" condition in the useEffect allows me to avoid loading secondary libraries like React Query or useSWR, while sticking to typical React-style code, and getting the proof-of-concept done. I'll handle those issues later when I get to them.

But now we understand why.

Exmaple code: I found another article explaining how to use useRef to avoid fetching if it's the first load for the component.

import { useState, useEffect, useRef } from 'react'
export default MyComponent() {
    const isMounted = useRef(false);
    const [storage, setStorage] = useState({})
    useEffect(() => {
        if (isMounted.current) {
            fetch(url, {
                headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
            })
            .then(response => response.json())
            .then(data => setStorage(data))
        } else {
            isMounted.current = true;
        }
    }, [])
    return (
        <>
            <h2>My Component</h2>
            <div>
                <p>Storage contains: {storage}</p>
            </div>
        </>
    )
}

From the React Docs Beta, they achieve this slightly differently (Fetching data).

  • The React Docs version produces two network requests, but the first gets ignored.
  • The useRef version avoids the secondary network request, but notice the useRef flag is scoped to the component, not only the useEffect() call.

useEffect(() => {
  let ignore = false;
  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }
  startFetching();
  return () => {
    ignore = true;
  };
}, [userId]);

Credits§