-1

I need to fetch a base64 encoded image in useEffect and then create an image tag in the page, my (simplified) code is like the following:

const App => {
    const [image, SetImage] = useState('');
    useEffect(
        async function fetchImage() {
            //...
            await response = fetch('API-to-get-base64image');
            SetImage(response);
        }, [])

    return (
        <div><img src={image}/> </div>
    )
}

I only need to run this useEffect once because the image is large and running it twice slows down the page loading. But as pointed in many posts like this one: How to call loading function with React useEffect only once, as of React 18, setting the dependency as an empty array no longer does this trick. I wonder what is the new solution to let this useEffect to run only once?

0

1 Answer 1

1

I had this same problem, until I read the Deep Dive section in Fetching data with Effects in the React docs.

The relevant bit is:

This list of downsides is not specific to React. It applies to fetching data on mount with any library. Like with routing, data fetching is not trivial to do well, so we recommend the following approaches:

  • If you use a framework, use its built-in data fetching mechanism. Modern React frameworks have integrated data fetching mechanisms that are efficient and don’t suffer from the above pitfalls.
  • Otherwise, consider using or building a client-side cache. Popular open source solutions include React Query, useSWR, and React Router 6.4+. You can build your own solution too, in which case you would use Effects under the hood but also add logic for deduplicating requests, caching responses, and avoiding network waterfalls (by preloading data or hoisting data requirements to routes).

In my case useSWR was already part of the project dependencies and looking at the example it is quite simple to use:

import useSWR from 'swr'
 
function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)
 
  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

Afterwards I verified that the Fetch only happens once, happy days...

So for your code that would be something like:

import useSWR from 'swr'

const App => {
    const [image, SetImage] = useState('');

    async function fetchImage(url) {
        //...
        await response = fetch(url);
        SetImage(response);
    }

    const { data, error, isLoading } = useSWR('API-to-get-base64image', fetchImage)

    return (
        <div><img src={image}/> </div>
    )
}
3
  • Thanks for your solution. I actually found a very simple way to do it. Simply check if image is empty at the first line inside useEffect, and immediately return if it already has a value.
    – SamTest
    Commented Jun 25 at 19:10
  • I'm not sure if checking the state would always work, if React tries to mount the component the second time while your request is in flight (probably more of a problem with a slow API rather than an image) it'll fire off a second request.
    – dain
    Commented Jun 25 at 21:54
  • Well in my real and much more complex code, I'd assign a value to another variable before I do the fetch. And what I actually checked is the value of that variable. Since it is not a fetch, it won't be "in flight". Yes, I agree with you that if it were the code I posted above, it is possible it may run twice. However, in this case, I think we can still set a sentinel variable just to prevent it from running the second time. Maybe not the most descent way to do it but get the job done simply and quick.
    – SamTest
    Commented Jun 26 at 0:53

Not the answer you're looking for? Browse other questions tagged or ask your own question.