How to fetch api via looped callbacks with React functional components

Viewed 557

So I have a 40+ loop that's calling another component to display images. Each image has an ID and with that ID I can get more information about the image like Name and description via another API call.

When DisplayImage gets called I want it to call another callback function that will send out API calls for that image's metadata, store it in a variable and display it as an H1 tag.

return (
 <div>

  {array.map(index) => {

   // Some Other Code That return a TokenID //

   <>
    {displayImage(tokenId)}
   </>
 
 </div>
})
 const displayImage = (tokenId) => {

  const imageName = GetURI(tokenId)

  return (
      <div className="token-container">
        <h1>{imageName}</h1>
        <img className="artwork" width="250px" src={`https://ipfs-asdf/${tokenId}`} />
      </div>
  )

 }

const GetURI = async (tokenId) => {

    const res = await fetch("https://api"+tokenId , {
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        },
    }).then(data => {
      console.log(data)
      return data.json();
    })
    .then(data => {
      return (data.name || [])
    })
    .catch(err => {
      console.log(err);
    });
}

The data is being displayed on the console but now I'm running into an infinite loop issue that I know UseEffect can solve but I can't quite figure it out. I managed to display the data on the console with UseEffect using the [] attribute but don't know how to display the data. Any help would be amazing. Thank you!

4 Answers

Two things useful to your situation

  • functions declared outside the component aren't recreated each render

  • useState and useEffect pairing limits calls to API to only when tokenId changes

// Put this function outside the component
// so it does not need a useCallback
// i.e not reconstructed each render of DisplayImage

const GetURI = async (tokenId) => {
  ...
});

const DisplayImage = (tokenId) => {

  const [imageName, setImageName] = useState()

  // limit calls to API to when tokenId changes
  // and if eslint complains add GetURI to dependency list
  // - but GetURI never changes, so no un-needed calls from it
  useEffect(() => {
    setImageName(GetURI(tokenId))
  }, [tokenId, GetURI])   
  
  return (
    <div className="token-container">
      <h2>{imageName}</h2>
      <img className="artwork" width="250px" src={`https://ipfs-asdf/${tokenId}`} />
    </div>
  )
};

You can also abstract to custom hook useImageName()

const GetURI = async (tokenId) => {
  ...
});

const useImageName = (tokenId) => {

  const [imageName, setImageName] = useState()

  useEffect(() => {
    setImageName(GetURI(tokenId))
  }, [tokenId, GetURI])   

  return imageName
})
const DisplayImage = (tokenId) => {

  const imageName = useImageName(tokenId)

  return (
    <div className="token-container">
      <h2>{imageName}</h2>
      <img className="artwork" width="250px" src={`https://ipfs-asdf/${tokenId}`} />
    </div>
  )
};

BTW in GetURI this

return (data.name || [])

looks like should be

return data.name || ''

Is a different approach ok? I'd put display image into its own component.

const DisplayImage = ({tokenId: {_tokenId}}) => {

  const imageName = GetURI(_tokenId)
  
  const GetURI = useCallback(async () => {
      await fetch("https://api"+tokenId , {
            headers: {
              'Content-Type': 'application/json',
              'Accept': 'application/json'
            },
        }).then(data => {
          console.log(data)
          return data.json();
        })
        .then(data => {
          return (data.name || [])
        })
        .catch(err => {
          console.log(err);
        });
      })
    });

    useEffect(() => {
      if (_tokenId) GetURI();
    }, [GetURI]);

  return (
      <div className="token-container">
        <h2>{imageName}</h2>
        <img className="artwork" width="250px" src={`https://ipfs-asdf/${_tokenId}`} />
      </div>
  )
};

and then

return (
 <div>

  {array.map(index) => {

   //Some Other Code//

   <DisplayImage tokenId={tokenId} />
 
 </div>
})

You should probably cache the response from GetURI(tokenId). No need to ask twice for the same URI when using the same tokenId. An easy way is using react-query:

Setup in App.js:

 // App.js
 import { QueryClient, QueryClientProvider } from 'react-query'

 const queryClient = new QueryClient()

 export default function App() {
   return (
     <QueryClientProvider client={queryClient}>
       <Example />
     </QueryClientProvider>
   )
 }

Then use in a DisplayImage component (instead of inline function):

// DisplayImage.js
import { useQuery } from 'react-query'

export function DisplayImage(tokenId) {
  const { isLoading, error, data: imageName } = useQuery(['images', tokenId], GetURI(tokenId))
  return (
    <div className="token-container">
      <h1>{isLoading ? 'loading...' : imageName}</h1>
      <img className="artwork" width="250px" src={`https://ipfs-asdf/${tokenId}`} />
    </div>
  )
}

I found the best way to go about it with everyones help on here so thanks!

I put the GetURI function inside the show image component, and had a useEffect method call GetURI every time there was a new token ID, then I set a state variable to whatever was returned.

No loops, no errors



const DisplayImage = (data) => {

  const [nftMetadata, setNftMetadata] = useState();

  const GetURI = async (data) => {
     const nftURI = await data.drizzle.contracts.Contract.methods.tokenURI(data.tokenId).call()

      await fetch(nftURI , {
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          "Access-Control-Allow-Origin": "*"
        },
        })
        .then(data => {
          return data.json();
        })
        .then(data => {
          return setNftMetadata(data || []);
        })
        .catch(err => {
          return console.log(err);
      });
  });

  useEffect(() => {
    GetURI(data);
  }, [data.tokenId])   
  
  return (
    <div className="token-container">

     <h2>{nftMetadata.name}</h2>
     <img className="artwork" width="450px" src={`https://ipfs:/whatever/${nftMetadata.image}`} />

    </div>
  );

};

return (
 <div>

  {array.map(index) => {

   // Some Other Code That returns a TokenID //

   <>
    <DisplayImage address={drizzle.contractList[0].address} tokenId={tokenId} drizzle={drizzle} drizzleState={drizzleState}/>
   </>
 
 </div>
})
Related