How to fetch api via looped callbacks with React functional components - reactjs

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!

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

useEffect React for function checkSession

At first it worked, but then it stopped.The problem is that the same action is performed on the server 4 times, apparently this is due to useEffect
My component:
function TableComp() {
const session = useCheckCookieToken()
return (
session !== null ?
<>
<Menu />
<AddService />
<DataTable />
</>
:
<Navigate to="/" />
)
}
My service function:
async function useCheckCookieToken() {
const [session, setSession] = useState()
useEffect(() => {
async function sessionPost() {
const res = await fetch(`/api/checkCookie`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
})
if (res.ok) {
const result = await res.json()
setSession(result)
}
}
sessionPost()
return () => { console.log('unmount') }
}, [])
return session
}
module.exports = {
useCheckCookieToken
}
My API:
I tracked that in the interval highlighted in the screenshot from 97 to 101 lines, the same action is triggered 4 times, apparently the problem is related to the problem of mounting

React: How to use received json data from firebase to make list of array?

I want to make a list of user details by filling the form with name, email, designation, phone & image input field, I've saved the form data in an object and sent the form data to it's parent using props addedUser(userData);, I'm using this addedUser prop in the parent component to send the data to the firebase and then use the same fetched data to make a list of users in array.
I've made loadedData empty array simply wanna push the data into it but the responseData.json() doesn't give me anything, the data is being saved to firebase perfectly but I'm facing problems using it.
Please check the code here and also the <Users /> component code where I'm trying to make the list:
Parent:-
function App() {
const [userData, setUserData] = useState([]);
const fetchData = useCallback(async () => {
try {
const responseData = await fetch(
"https://react-users-db-default-rtdb.asia-southeast1.firebasedatabase.app/users.json"
);
const data = responseData.json();
console.log(data);
const loadedData = [];
for (const key in data) {
loadedData.push({
key: key,
userName: data[key].userName,
userEmail: data[key].userEmail,
userDesignation: data[key].userDesignation,
usePhone: data[key].usePhone,
userImage: data[key].userImage,
});
}
console.log(loadedData);
setUserData(loadedData);
} catch (err) {
console.log(err);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
async function recieveUserData(users) {
const responseData = await fetch(
"https://react-users-db-default-rtdb.asia-southeast1.firebasedatabase.app/users.json",
{
method: "POST",
body: JSON.stringify(users),
headers: {
"Content-Type": "application/json",
},
}
);
const data = await responseData.json();
console.log(data);
}
return (
<main>
<div className="form__wrap">
<AddUserForm addedUser={recieveUserData} />
</div>
<div className="user__wrap">
{userData.length > 0 ? (
<Users newUser={userData} />
) : (
<p>No user found</p>
)}
</div>
</main>
);
}
Users Component:
export default function Users({ newUser }) {
console.log(newUser);
return (
<div className="usercard__wrap">
{newUser.map((el, i) => {
return (
<UserCard
key={i}
name={el.userName}
email={el.userEmail}
designation={el.userDesignation}
phone={el.userPhone}
image={el.userImage}
/>
);
})}
</div>
);
}
Data is going to firebase:-
But responseData.json() is giving me this, I can't see my user object to access:-
In your fetchData function you need to await responseData.json().
const data = await responseData.json();

json response from mock server not printing but is in the console

I am trying to learn react, and I am making a successful API call, but it only prints in the console. I found examples but many of them recommended to use setData(json) but I am not able to use it because the file is a list of export async function which was also recommended.
export async function GetHellWorld() {
return fetch(`http://localhost:8080/api`, {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(response => response.json())
.then(json => {
console.log(json)
})
.catch(error => (console.log(error)))
}
and the component
function Test(thisArg, argArray) {
const result = GetHellWorld.apply()
return (
<div className="App">
{JSON.stringify(result)}
</div>
);
}
export default Test;
In the console I see "Hello World" but in the browser is get just {}.
Two questions:
How can I bind the JSON response to an object so I can do something like result.name.
Is this the correct was to call the await function? const result = GetHellWorld.apply()
---- update ----
I decided to try axios because I want to make multiple calls in one file.
const axios = require('axios');
export class AppService {
public async GetHelloWorld(): Promise<any> {
const response = await axios.get(`http://localhost:8080/api`, {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).catch(() => console.log("Issue in GetHelloWorld"))
return response.data
}
}
component
import React from 'react';
import {AppService} from "../services/app.service";
function Movies() {
const api = new AppService()
const hello = async () => {
const response = await api.GetHelloWorld();
console.log("The response: " + response)
}
return (
<div className="App">
{JSON.stringify(hello)}
</div>
);
}
note I had to add typescript support.
For whatever reason I get
Module not found: Error: Can't resolve '../services/app.service' in '/Users/miketye/programming/test-react/src/components'
While the other answer about using a custom hook can work, I would not recommend it while you're still leaning React.
Look up how to use the "useEffect" hook, that's generally how you want to do any sort of loading logic in React.
First off, you need to fix your async function so it actually returns a value:
// style/convention note, but non-component functions should not start with a capital letter
export async function getHelloWorld() {
return fetch(`http://localhost:8080/api`, {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(response => response.json())
.then(json => {
return json // will cause this function to return a Promise of type "string", since we're in an async function
})
// better to just let the error get thrown here, for testing
}
Then use it like this:
function Test(thisArg, argArray) {
[fetchResult, setFetchResult] = useState(undefined) // look up useState. State is how you have values that change over time in a resct component
useEffect(() => {
async function fetchData() {
const data = await getHelloWorld()
setFetchResult(data)
}
fetchData()
}, [])
// look up useEffect. Since the second argument (the "dependency array") is empty, useEffect will fire only once, after the component loads
return (
<div className="App">
{result ? JSON.stringify(result) : "no result yet"}
</div>
);
}
export default Test;
You can use a custom hook for this purpose:
import { useState } from "react";
const useFetchData = () => {
const [data, setData] = useState(null);
const fetchData = () => {
fetch("http://localhost:8080/api", {
method: "Get",
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(response => response.json())
.then(json => { setData(json); })
.catch(error => { console.log(error); })
}
useEffect(() => {
fetchData();
}, []);
return { data, fetchData };
}
export default useFetchData;
And then call it in your component:
import useFetchData from "#/hooks/useFetchData";
const Test = () => {
const { data, fetchData } = useFetchData();
// CALL fetchData IF YOU WANT TO UPDATE THE CURRENT STATE
return (
<div className="App">
{data && JSON.stringify(data)}
</div>
);
}
export default Test;

react error unknown : TypeError: Cannot read properties of undefined

i am getting data from api (i know api is working) and i am getting error (error is after my code)
i think error is because that in first place that code runs there is no "product.data.attributes.name" and that error create please help me!
const ProductDetails = () => {
const { params } = useRouteMatch();
const productCtx = useContext(ProductContext);
const [product,setProduct] = useState({});
const getProduct = useCallback(async () => {
try {
productCtx.toggleIsLoading()
const response = await fetch(
`http://localhost:1337/api/products/${params.id}?populate=*`,
{
method: "GET",
headers : {
'content-type' : 'application/json'
}
})
if (!response.ok) {
console.log(response)
throw new Error('Something went wrong!');
}
const data = await response.json();
productCtx.toggleIsLoading()
console.log(data.data.attributes.name);
setProduct(data)
}catch (error){
console.log(error)
}
},[])
useEffect(() => {
getProduct()
},[getProduct])
useEffect(() => {
console.log(product.data.attributes.name);
}, [product]);
return(
<Fragment>
<section className="single-product">
<img src={product.img} alt={product.img} className="single-product-image"/>
<article>
<h1>{product.data.attributes.name}</h1>
<h2>${product.data.attributes.price}</h2>
<p>{product.data.attributes.description}</p>
</article>
</section>
</Fragment>
);
}
this is the error
this is the error message
You can make render condition.
const ProductDetails = () => {
const { params } = useRouteMatch();
const productCtx = useContext(ProductContext);
const [product,setProduct] = useState({});
const getProduct = useCallback(async () => {
try {
productCtx.toggleIsLoading()
const response = await fetch(`http://localhost:1337/api/products/${params.id}?populate=*`,
{
method: "GET",
headers : {
'content-type' : 'application/json'
}
})
if (!response.ok) {
console.log(response)
throw new Error('Something went wrong!');
}
const data = await response.json();
productCtx.toggleIsLoading()
console.log(data.data.attributes.name);
setProduct(data)
} catch (error){
console.log(error)
}
},[])
useEffect(() => {
getProduct()
},[getProduct])
useEffect(() => {
console.log(product.data.attributes.name);
}, [product]);
return(
<Fragment>
<section className="single-product">
<img src={product.img} alt={product.img} className="single-product-image"/>
<article>
<h1>{product.data?.attributes?.name || ''}</h1>
<h2>${product.data?.attributes?.price || ''}</h2>
<p>{product.data?.attributes?.description || ''}</p>
</article>
</section>
</Fragment>
);
}
Or You can make loading before product.data is available.
if(!product.data) return <div>Loading</div>
return(
<Fragment>
<section className="single-product">
<img src={product.img} alt={product.img} className="single-product-image"/>
<article>
<h1>{product.data?.attributes?.name || ''}</h1>
<h2>${product.data?.attributes?.price || ''}</h2>
<p>{product.data?.attributes?.description || ''}</p>
</article>
</section>
</Fragment>
);
}

Why am I unable to set an array in useEffect?

I am learning React and trying to write an asynchronous hook. Using setResult inside of useEffect doesn't seem to work. When I tried to render the result, there was nothing, so I added some console logging to see what is going on. The setter function in the useState hook doesn't seem to be doing anything. I've been following this video for some guidance, and my code does not differ too much.
I have the following component:
import React, { useState, useEffect } from 'react'
const Search = () => {
const [search, setSearch] = useState('')
const [query, setQuery] = useState('')
const [result, setResult] = useState([])
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(
`https://api.spotify.com/v1/search?q=${encodeURIComponent(
query
)}&type=track`,
{
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + auth.access_token
}
}
)
const json = await response.json()
console.log({ json })
console.log(
json.tracks.items.map(item => {
return item.id
})
)
setResult(
json.tracks.items.map(item => {
return item.id
})
)
console.log({result})
} catch (error) {
console.log(error)
}
}
if (query !== '') {
fetchData()
}
}, [query])
return (
<div>
<input
value={search}
placeholder='Search...'
onChange={event => setSearch(event.target.value)}
onKeyPress={event => {
if (event.key === 'Enter') {
setQuery(search)
}
}}
></input>
<br />
{result.map(item => (
<h3 key={item}></h3>
))}
</div>
)
}
export default Search
From console.log ({ json }), I see the response from the server looks OK.
console.log(
json.tracks.items.map(item => {
return item.id
})
)
The above console output looks OK as well.
setResult(
json.tracks.items.map(item => {
return item.id
})
)
console.log({result})
Why is result empty?
EDIT: Thanks Patrick and Talgat. I understand now. So, when I console.log outside of useEffect, I could see result is set correctly. I then realized I was missing a reference to {item} in my render:
{result.map(item => (
<h3 key={item}>{item}</h3>
))}
Now I see the IDs rendered on the page. Thanks for your help.
setTimeout(()=>{
console.log(result);
},0);
Please try this instead using console.log(result).
Setting state is not updated as soon as you insert value via setter( on your side setResult ).
So to do it, delay is needed.

Resources