Working on a project here and ran into an issue. I haven't had this problem before but now I do for some reason.
So I am making an GET request to ASOS API, but it is acting very strange. Some of these, such as name is received upon page refresh, but mainly the other things like information about the brand becomes undefined. Now, the brand is in another object inside of the API. But I have had other things at other parts of the page before that were also in objects. But I did not have any issue there.
Here is how the API call looks like:
And here is my code for the API fetch:
const FetchAPI = (props) => {
const [product, setProduct] = useState({});
const [brand, setBrand] = useState({});
const [price, setPrice] = useState({});
const [params, setParams] = useState({
id: "23363645",
lang: "en-US",
store: "US",
sizeSchema: "US",
currency: "USD",
});
useEffect(() => {
const options = {
method: "GET",
url: "https://asos2.p.rapidapi.com/products/v3/detail",
params: params,
headers: {
"x-rapidapi-key": "",
"x-rapidapi-host": "",
},
};
axios
.request(options)
.then(function (response) {
console.log(response.data);
setProduct(response.data);
setBrand(response.data.brand);
setPrice(response.data.price.current);
})
.catch(function (error) {
console.error(error);
});
}, []);
return (
<div>
<Product
name={product.name}
price={price.text}
brand={brand.description.replace( /(<([^>]+)>)/ig, '')}
/>
</div>
);
};
I am sending the data over to Product which is the product page of the item I am requesting. But whenever I refresh the page, I get TypeError: Cannot read property 'replace' of undefined. I did remove replace, and it worked fine. And if I placed replace back into the brand.description and saved, still worked fine. But on the page refresh, it crashes.
Is it perhaps trying to load my return before the useEffect? If so, how do I solve this?
I think the issue here is that while data from API is being fetched, brand.description is undefined and there is no replace method on undefined. You can either do this - >
<div>
<Product
name={product.name}
price={price.text}
brand={brand.description ? brand.description.replace( /(<([^>]+)>)/ig, '') : ""}
/>
</div>
or
const [brand, setBrand] = useState({ description: ""});
and keep the remaining code same.
First of all, unrelated to your question, you have many superfluous state variables. You have product which stores all the data of the product and then price and brand which stores subsets of the same data.
Consider using only the product state variable, or if you want to keep the names do something like
const price = product.price.current;
Second, your default value for brand is an empty object, meaning brand.description is undefined.
You can solve this with optional chaining like so:
<Product
name={product?.name}
price={price?.text}
brand={brand?.description?.replace( /(<([^>]+)>)/ig, '')}
/>
The useEffect hook gets called once the component is rendered so in this case initially when your API is not called your brand.description will be undefined and when you are trying to use replace on undefined the error is coming. So you can always add a check using optional chaining(?.) so even if we don't get the description in the brand it will not break the website and you should also use a loader till the API call is through.
<div>
<Product
name={product.name}
price={price.text}
brand={brand.description?.replace( /(<([^>]+)>)/ig, '')}
/>
</div>
useEffect gets called only after the component is rendered . So when the component is rendered for the first time you have the state brand as an empty object . So what you are trying to do is {}.description -> . This is undefined .
This is the reason why its a good practice to always have a loading state when the component is making an api call.
const [ loading, setLoading ] = useState(true);
const getProductDetails = async() => {
const options = {
method: "GET",
url: "https://asos2.p.rapidapi.com/products/v3/detail",
params: params,
headers: {
"x-rapidapi-key": "",
"x-rapidapi-host": "",
},
};
try {
const { data } = await axios.request(options);
setProduct(data);
setBrand(data.brand);
setPrice(data.price.current);
}
catch(error){
console.log(error)
} finally {
setLoading(false)
}
}
useEffect(() => {
getProductDetails();
}, []);
if(loading)
return <p> Loading ... </p>
return (
// return your JSX here
)
Related
I currently have some issues trying to add the infinite query feature to a recipes app I'm working on using Edamam API.
All the examples I have looked for (even React Query's documentation) implement the infinite scroll using a page/cursor number system... I understand this is the ideal way, but... Edamam API doesn't work this way with paginated queries.
Instead, the API has the following structure for each recipe query we look for (let's assume we are searching for "chicken", this would be the JSON structure):
from: 1,
to: 20,
count: 10000,
_links: {
next: {
href: "https://api.edamam.com/api/recipes/v2?q=chicken&app_key=APIKEYc&_cont=CHcVQBtNNQphDmgVQntAEX4BYldtBAAGRmxGC2ERYVJ2BwoVX3cVBWQSY1EhBQcGEmNHVmMTYFEgDQQCFTNJBGQUMQZxVhFqX3cWQT1OcV9xBB8VADQWVhFCPwoxXVZEITQeVDcBaR4-SQ%3D%3D&type=public&app_id=APPID"
title: "Next Page"
}
},
hits: [{}] ... (This is where the actual recipes are)
As you can see, there is no numbering system for paginated queries, instead, it's a whole URL and it's giving me a hard time since I'm also new to React Query.
I tried the following, but it just fetches the same data over and over again as I reach the bottom of the page:
const getRecipes = async ({ pageParam }) => {
try {
const path = pageParam
? pageParam
: `https://api.edamam.com/api/recipes/v2?q=${query}&app_id=${process.env.REACT_APP_APP_ID}&app_key=${process.env.REACT_APP_API_KEY}&type=public`;
const response = await axios.get(path);
return response.data;
} catch (error) {
console.log(error);
}
const { ref, inView } = useInView();
useEffect(() => {
inView && fetchNextPage();
}, [inView]);
const {
data,
isFetching,
isFetchingNextPage,
error,
status,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery(
["recipes", query],
({ pageParam = "" }) => getRecipes(pageParam),
{
getNextPageParam: (lastPage) => lastPage._links.next.href,
}
);
Since the next page param is a whole URL, I just say that IF there is a pageParam, then use that URL for the request, if not, then do a normal request using the query value the user is searching for.
Please help!
Since the next page param is a whole URL, I just say that IF there is a pageParam, then use that URL for the request, if not, then do a normal request using the query value the user is searching for.
I'd say that this is the correct approach. The only code issue I can see in your example is that you destruct page param, and then pass the page param string to getRecipes:
({ pageParam = "" }) => getRecipes(pageParam),
but in getRecipes, you expect an object to come in (which you again destructure):
const getRecipes = async ({ pageParam }) => {
You can fix that by either changing the call side, or the function syntax, and then it should work.
is there a way to persist data from useLocation() ?
Basically I'm sending data that is located on a nested collection from firebase
(There's a lot of background on my question/explanation, you can just skip to the important bit)
Usually you have no issues when you only have one collection you can always access the UID pretty much from anywhere by sending it from the App.js for example:
function App() {
const [user, setUser] = useState([]);
useEffect(() => {
auth.onAuthStateChanged((authUser) => {
if (authUser) {
setUser(authUser);
} else {
setUser(false);
}
})
}, [])
return (
< div >
<Router>
<Switch>
<Route path = "/SomePath">
<SomeLocation user={user}/>
</Route>
</Switch>
</Router>
</div >
);
}
export default App;
and since the user have all the data you need to get any other piece of information (usually) you don't need to worry about nested collections however, what if I'm using nested collections ?
if you want to access all the data from a nested collection that's also fine you do not require any other extra information apart from the user
Ej:
useEffect(() => {
const dbRef= db.collection("users").doc(user.uid).collection("someCollection")
dbRef.onSnapshot(snapshot => {
const tempData = [];
snapshot.forEach((doc) => {
const data = doc.data();
tempData.push(data);
});
setDataSomewhere(tempData);
})
}, [user]);
However how you get the data of the uid of an specific document inside a nested location with just the user ? you can't (as far as I'm aware)
The Important bit without all the background
EJ:
if you have let's say a "parent" and he adds "students" which is my case:
and I wanted to edit this "student" that is in a nested collection, let's make an example on the first one the one named "alfonsa" this is the edit form which is in another "/Path"
The way I'm handling the data of the student is the following, when you check the student and you select the edit icon from the data-table it sends some data through useLocation()
//PATH-A (The one with the data table)
const editStudent= {
pathname: '/EDIT_STUDENT',
data: studentData
}
const editStudents= () => {
if(studentData== 0)
{
window.alert("Select a student")
}
else {
history.push(editStudent);
}
};
///EDIT_STUDENT (Edit form)
let studentData= useLocation();
let data = studentData.data;
console.log(data)
const uid = data[0].uid <- Here I get that student specific uid
This is what the console log returns (All correct data):
However all the data disappears on refresh (which makes sense because is no longer getting the useLocation data from the previous "/path" location) this is the question:
How can I keep/store/maintain that data that comes from the useLocation() on refresh
Any help/tip/example is welcome
Forgot to add this
This is basically what comes up when I refresh
UPDATE So I tried localStorage() as someone mention but now I can't access the data because is coming as a string how can I separate the data ?
This is how I'm storing the data:
useEffect(() => {
const localStorageEstData = window.localStorage.getItem("students");
localStorageEstData && setStudentsData(JSON.parse(localStorageEstData));
}, []);
useEffect(() => {
window.localStorage.setItem("students", JSON.stringify(studentsData));
}, [studentsData]);
This is how I'm getting the data + how it shows in console:
let data = window.localStorage.getItem("estudiantes")
is coming as a whole string how can I separate it ?
Since no one posted an answer I will based on what #Daniel Beck said
Instead of using useLocation() that is to pass data from one /Path to another is better to use localStorage for multiple reasons and the more important one is to persist/keep data on refresh.
if you use useLocation() and you refresh the data will disappear because is no longer parsing data from one /Path to another. However the localStorage will persist on refresh which is why is more useful for this case
simple solution for me was:
useEffect(() => {
const localStorageEstData = window.localStorage.getItem("students");
localStorageEstData && setStudentsData(JSON.parse(localStorageEstData));
}, []);
useEffect(() => {
window.localStorage.setItem("students", JSON.stringify(estudiantesData));
}, [studentsData]);
and then in the other end:
const data = JSON.parse(window.localStorage.getItem("students"))
const uid = data[0].uid
that way I have access to all the information not just the uid
My problem is that item stored in object (in an array of objects inside a state hook) is not being rendered on page, but it gets printed with console.log
I fetched some data from the server and it worked as expected, returning an array of two items, one of which is an object containing blog data(blog heading, creator, etc) an another is an array of 'sections' of that blog. Here is how I did it,
This is the initialization
// Get the blog id
const {blog_id} = useParams();
// Declaring our state hooks
const initial_blog_state = {
blog_id: blog_id,
heading: '',
creator: {},
created: '',
section: [],
}
const [blog_state, updateBlogState] = useState(initial_blog_state);
Here is the fetching of data from the server
useEffect(() => {
// Fetching data
Promise.all([
fetch(`http://127.0.0.1:8000/api/blog/${blog_id}`),
fetch(`http://127.0.0.1:8000/api/section/full/${blog_id}`)
]).then(responses => {
// Get a JSON object from each of the responses
return Promise.all(responses.map(response => {
return response.json()
}))
}).then(function (data) {
// Log the data to the console
console.log(data);
// Update state
updateBlogState({
...blog_state,
heading: data[0].heading,
creator: data[0].creator,
created: data[0].created,
section: data[1]
})
}).catch(function (error) {
// if there's an error, log it
console.log(error);
});
}, []);
I think the way I'm updating the section inside the hook can be a problem(although I'm not sure how), because I saw in a stackoverflow answer that objects must always be initialized (which I'm not doing when declaring an array of 'objects')
And here is the thing that needs to be rendered
return (
<div className="container">
<h1>{blog_state.heading}</h1>
<p className="text-small text-muted">{blog_state.creator.username}</p>
{blog_state.section.map(item => {
{console.log(item.paragraph)}
<p>{item.paragraph}</p>
})}
</div>
)
Here blog_state.heaing and blog_state.creator.username are being rendered as desired and also console.log(item.paragraph) prints the correct paragraph on the console window, but item.paragraph doesn't show anything on the page.
Nothing is being returned from your map.
i.e you need to add a return line before the <p>{item.paragraph}</p>
So:
{blog_state.section.map(item => {
console.log(item.paragraph)
return <p>{item.paragraph}</p>
})}
or as an inline return:
{blog_state.section.map(item => <p>{item.paragraph}</p>)}
hope you're well. I've been working on a company list component. I am having a problem with updating it in real time, because the axios call I'm sending to 'getById' after it renders is returning only a promise and not the actual data that it is supposed to and I don't have any idea as to why. So when I push the so called new company that I've just added into the array, which is in state, it is only pushing a promise into the array and not the actual Company. I don't have any idea why this is. What the code is supposed to be doing, is it is supposed to be putting the new company into the database, returning the success result, and then I'm using the item from that to make a fresh get call to the axios DB which is supposed to be returning the information I just entered so that I can then insert it into the same array in state that is within the list that is rendered in the company list. However, as I mentioned, only the promise is coming up for some reason.
At one point I was able to get this working, but I did that by essentially calling, 'componentDidMount' after the promise was pushed into the call back clause of the setState funciton of the push function - which was essentially causing the entire component to re-render. I'm a fairly new coder, but my understanding is is that that is a fairly unconventional way to code something, and contrary to good coding methodologies. I believe I should be able to push it into state, and then have it change automatically. I have attached the relevant code below for you to examine. If you believe you need more please let me know. If someone could please tell me why I am getting this weird promise instead of the proper response object, so that I can then insert that into state, I would greatly appreciate it. I've attached some images below the code snippets that I hope will be helpful in providing an answer. I have also left brief descriptions of what they are.
:
class Companies extends React.Component {
constructor(props) {
super(props);
this.state = {
Companies: [],
formData: { label: "", value: 0 },
};
}
componentDidMount = () => {
this.getListCompanies();
};
getListCompanies = () => {
getAll().then(this.listOfCompaniesSuccess);
};
listOfCompaniesSuccess = (config) => {
let companyList = config.items;
this.setState((prevState) => {
return {
...prevState,
Companies: companyList,
};
});
};
onCompListError = (errResponse) => {
_logger(errResponse);
};
mapCompanies = (Companies) => (
<CompaniesList Companies={Companies} remove={remove} />
);
handleSubmit = (values) => {
if (values.companyName === "PPP") {
this.toastError();
//THIS IS FOR TESTING.
} else {
add(values)
.getById(values.item) //I HAVE TRIED IN TWO DIFFERENT PLACES TO GET THE NEW COMPANY IN. HERE
.then(this.newCompanyPush)
.then(this.toastSuccess)
.catch(this.toastError);
}
};
//THIS CODE RIGHT HERE IS THE CODE CAUSING THE ISSUE.
newCompanyPush = (response) => {
let newCompany = getById(response.item); // THIS IS THE OTHER PLACE I HAVE TRIED.
this.setState((prevState) => {
let newCompanyList = [...prevState.Companies];
newCompanyList.push(newCompany);
return {
Companies: newCompanyList,
};
});
};
toastSuccess = () => {
toast.success("Success", {
closeOnClick: true,
position: "top-right",
});
};
toastError = (number) => {
toast.error(`Error, index is ${number}`, {
closeOnClick: true,
position: "top-center",
});
};
This is the axios call I am using.
const getById = (id) => {
const config = {
method: "GET",
url: companyUrls + id,
withCredentials: true,
crossdomain: true,
headers: { "Content-Type": "application/json" },
};
return axios(config).then(onGlobalSuccess).catch(onGlobalError);
};
After the promise is pushed into the array, this is what it looks like. Which is I guess good news because something is actually rendering in real time.
This is the 'promise' that is being pushed into the array. Please note, when I make the same call in post-man, I get an entirely different response, see below.
This is the outcome I get in postman, when I test the call.
I'm trying to use an array returned from Node in a React code.
Here's the Node's router code:
router.get('/prodlist', async (req, res) => {
try {
await Product.find({}).exec((err, result) => {
res.status(200).send({ express: result })
})
} catch (e) {
console.log(e)
res.status(400).send(e)
}
})
The React's component code:
import React, { useState, useEffect } from 'react';
function App() {
const [datas, setDatas] = useState(null)
useEffect(() => {
var curURL = window.location.href
var calledRoute = curURL.substr(8).substr(curURL.substr(8).indexOf("/"))
callBackendAPI(calledRoute)
.then(res => setDatas(prevData => res.express))
.catch(err => console.log(err));
}, [])
async function callBackendAPI(routePath) {
const response = await fetch(routePath);
const body = await response.json();
if (response.status !== 200) {
throw Error(body.message)
}
console.log(body.express)
return body;
}
return (
<div className="App">
{{datas}}
</div>
)
}
export default App;
And the array content (seen from Chrome's developer tools):
(3) [{…}, {…}, {…}]
0:
prodDesc: "Prod1"
prodID: "1"
prodImage: "prod1.jpg"
prodPrice: 350
prodRebate: 100
prodReviews: []
prodStock: 31
prodThumb: "prod1.jpg"
__v: 0
_id: "5eb04a6439eec26af4981541"
__proto__: Object
1: {prodID: "2", prodDesc: "Prod2", prodThumb: "prod2.jpg", prodImage: "prod2.jpg", prodPrice: 1500.79, …}
2: {prodID: "3", prodDesc: "Prod3", prodThumb: "prod3.jpg", prodImage: "prod3.jpg", prodPrice: 280.79, …}
length: 3
__proto__: Array(0)
When I execute this code I get the error: Objects are not valid as a React child
So I tried to map data's content with a line like this:
{datas.map((item) => <p>{item}</p>)}
Executing this code with a map I get the error : Cannot read property 'map' of null.
Although datas contains an array, it seems React sees it as null.
Anybody understands this issue?
EDIT: I didn't realized that this question is about two different issues. Norbitrial's answer fixed the problem about synchronicity but I still get the error "Objects are not valid as a React child [...] use an array instead".
EDIT2: Solved the issue by forwarding the mapping to a REACT component with the following line:
{datas && datas.map(item => <ShopCart key={item.prodID} item={item} />)}
The problem is your backend API returns the result asynchronously so it might take few seconds while your state value is null.
The solution can be checking for null value first then using the .map() on your state called datas with && in your code.
Try as the following:
{datas && datas.map((item) => <p>{item}</p>)}
I hope this helps!
The code runs before datas is populated from your api, you need to first check to make sure it is populated like this.
{datas && datas.length && datas.map((item) => <p>{item}</p>)}
The problem is that your component gets mounted before you receive the data.
What I would do(and most people) is to create a loading component that will show a spinner or something like that when the array is empty and after it gets filled it should render the component with the data.
But a quick solution to this is to do a conditional render
{datas.length && datas.map((item) => <p>{item}</p>)}