React, Cannot read properties of undefined Error with useEffect - reactjs

I'm trying to get some weather data from an API, but I always get the same error of not being able to read properties of undefined. I've gone through different tutorials and previously asked issues, but I haven't been able to figure out what I'm doing wrong. Could anyone please give me a hand?
export default function Weather(){
const apiKey = process.env.REACT_APP_API_KEY
const weatherUrl = `http://api.weatherapi.com/v1/current.json?key=${apiKey}&q=Saxthorpe&aqi=no`
const [weatherData, setWeatherData] = useState();
const [error, setError] = useState(null);
useEffect(() => {
(
async function(){
try {
const response = await axios.get(weatherUrl);
setWeatherData(response.weatherData);
} catch (error) {
setError(error);
}
}
)();
}, [])
return (
<div className="weather-feature">
<h1>hi</h1>
<p className="location">{weatherData.location.name}</p>
<p className="temp">{weatherData.current.temp_c}</p>
<p className="weather-desc">{weatherData.current.condition.text}</p>
</div>
)
}

When pulling data like this and rendering components conditional on that data, you should account for situations in which the data is not yet available or null.
Specifically, you're attempting to render this data:
return (
<div className="weather-feature">
<h1>hi</h1>
<p className="location">{weatherData.location.name}</p>
<p className="temp">{weatherData.current.temp_c}</p>
<p className="weather-desc">{weatherData.current.condition.text}</p>
</div>
But it's not going to available on the first render (i.e. weatherData does not have a location property at first, since your default useState value is undefined).
There are many ways around this, and what you choose ultimately depends on your project and preferences.
You can use optional chaining as a simple protection against null references when checking nested properties:
return (
<div className="weather-feature">
<h1>hi</h1>
<p className="location">{weatherData.location?.name}</p>
<p className="temp">{weatherData.current?.temp_c}</p>
<p className="weather-desc">{weatherData.current?.condition?.text}</p>
</div>
Or you can return something else if weatherData is not ready. A good tool for this kind of thing is swr:
import useSWR from 'swr'
function Weather()
{
const { weatherData, error } = useSWR(weatherUrl, fetcher)
if (error) return <div>failed to load</div>
if (!weatherData) return <div>loading...</div>
return <div>hello {weatherData.location}!</div>
}
As a side note, another thing to consider is your useEffect dependencies:
useEffect(() => {
(
async function(){
try {
const response = await axios.get(weatherUrl);
setWeatherData(response.weatherData);
} catch (error) {
setError(error);
}
}
)();
}, [])
With an empty dependency array, your effect runs only on mount and unmount. If you want it to run based on some other variable(s) changing, add those variables to the dependency array.

You can debug to check the response. I think the respose is undefined from
const response = await axios.get(weatherUrl);
response = undefined => can not get weatherData property.
We are using useEffect you can debug on it by F12 in Chrome and see what happen and the reason of this bug. This is better than you come here to ask

Look: weatherData is your state, which is initially... nothing, because you don't pass any data.
So, you cannot access the location field on the first render because it does not exist yet.
It would help if you made sure weatherData exist:
return (
<div className="weather-feature">
<h1>hi</h1>
<p className="location">{weatherData?.location.name}</p>
<p className="temp">{weatherData?.current.temp_c}</p>
<p className="weather-desc">{weatherData?.current.condition.text}</p>
</div>
)

Related

How to implement error boundaries in react with MERN?

My goal is to simply dynamically present the data from a mongo database of a specific document.
const Details = () => {
const { id } = useParams()
const [product, setProduct] = useState(null)
useEffect(() => {
const fetchProduct = async () => {
const response = await fetch(`/api/products/${id}`)
const json = await response.json()
if (response.ok) {
setProduct(json)
console.log(json)
}
}
fetchProduct()
}, [id])
this code works fine as it gets the product, but my problem is occurring with the rendering:
return (
<div className="details">
<Menu />
<h1 className="movement">Product Details - {product.name}</h1>
</div>
);
}
the error that I'm getting is Uncaught TypeError: Cannot read properties of null (reading 'name') and to Consider adding an error boundary to your tree to customize error handling behavior.
my question being is how do i implement correct error handling
Just use the optional chaining operator:
product?.name
It's typical that in the first render product is not yet available so you cannot access to any of its properties. With the optional chaining operator you are covering this case.
See more: https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/Optional_chaining
If you want to add Error Boundaries:
https://reactjs.org/docs/error-boundaries.html#gatsby-focus-wrapper
React renders the component before you make an api request, thus product doesn't exist (it's null based on how you set its initial value). Then when response is received you set product to state and react re-renders your component. Now product exists.
To solve it, render h1 only when product exist.
<div className="details">
<Menu />
{ product && <h1 className="movement">Product Details - {product.name}</h1> }
</div>
Also, you can render a message if product is not yet exist
<div className="details">
<Menu />
{ product && <h1 className="movement">Product Details - {product.name}</h1> }
{ !product && <p>Loading ... </p>
</div>

Unable to loop single react component

I'm trying to loop my StudentInfo component so a blurb of information can be repeated down an array of 25 objects provided through an API for each object/a student (name, email, company, etc.) I'm not
sure where I'm going wrong; here is my attempted loop via the map function:
export default function StudentList() {
let students = useState(null);
return (
<div className="StudentList">
<div className="row">
<div className="col">
{students.map(function (student, index) {
if (index <= 25) {
return (
<div className="col" key={index}>
<StudentInfo data={student} />
</div>
);
}
})}
</div>
</div>
</div>
);
}
Can someone see what I'm missing or what I might have skipped? I usually assume that I must be doing something wrong because I'm still new at React and trying to adapt other code I used for a weather forecast app to be used for this, but I don't think it's translating over well.
When I run this, I see the first object twice, i.e. Name, Email, Company, etc. it shows the info for the same person twice, rather than the first person, the second person, etc. I want to be able to see this pull all the objects from the array.
Here is the information I'm pulling to return as an entry on student info:
export default function StudentInfo() {
const [info, addInfo] = useState(" ");
function setInfo(response) {
addInfo({
number: response.data.students[0].id,
first: response.data.students[0].firstName,
last: response.data.students[0].lastName,
email: response.data.students[0].email,
company: response.data.students[0].company,
skill: response.data.students[0].skill,
average: response.data.students[0].grades[0],
});
}
let url = "https://api.hatchways.io/assessment/students";
axios.get(url).then(setInfo);
return (
<div className="StudentInfo">
<h1>{info.number}.</h1>
<h2>
Name: {info.first} {info.last}
</h2>
<h2>Email: {info.email}</h2>
<h2>Company: {info.company}</h2>
<h2>Skill: {info.skill}</h2>
<h2>Average: {info.average}</h2>
</div>
);
}
I'm using "if (index <= 25)" as there are 25 total entries that I want showing, but as I mentioned, I have no doubt I'm going about this incorrectly. I want this to loop through all 25 objects with the above information, as I keep saying. I'm sorry if I'm not speaking technically enough to be understood, as I am still learning.
I just want this to return 25 times with info from each object so that it's all listed out.
This is what it currently looks like
UPDATE
I've been tinkering and was able to repeat the entry, however I'm now having trouble getting the unique information, i.e. I'm only seeing the first entry over and over. I'm not sure how to reiterate with new information since it's only going to response.data.students[0]. This is what my code looks like now, where the mapping is:
export default function StudentList() {
let [loaded, setLoaded] = useState(false);
let [students, setStudent] = useState(" ");
function doInfo(response) {
console.log(response.data);
setStudent(response.data.students[0].id);
setLoaded(true);
}
if (loaded) {
return (
<div className="StudentList">
<div className="row">
<div className="col">
{students.map(function (id) {
return <StudentInfo data={id} />;
})}
</div>
</div>
</div>
);
} else {
let url = "https://api.hatchways.io/assessment/students";
axios.get(url).then(doInfo);
}
}
Can someone help me code it so it runs through all 25 entries, not just the first one?
There are some errors in your code.
UseState
React useState hook returns an array with a value and a setter method to update the state useState docs.
const [students, setStudents] = useState(null);
Iterate over null values
If your state starts with a null value you will not be able to iterate over it. To avoid getting an error you should make sure to use the map operator when your state has a value.
{students && students.map(function (student, index) {
...
})}
Handling side effects
You should move your API request (and set your info state) inside of a useEffect (useEffect docs). This way you will set those values asynchronously after the component is mounted.
export default function StudentInfo() {
const [info, addInfo] = useState(null);
useEffect(() => {
async function fetchInfo() {
const url = "https://api.hatchways.io/assessment/students"; //should be a constant outside the component
const response = await axios.get(url);
addInfo({
number: response.data.students[0].id,
first: response.data.students[0].firstName,
last: response.data.students[0].lastName,
email: response.data.students[0].email,
company: response.data.students[0].company,
skill: response.data.students[0].skill,
average: response.data.students[0].grades[0],
});
fetchInfo();
}, [])
return (
<div className="StudentInfo">
{info &&
<h1>{info.number}.</h1>
<h2>
Name: {info.first} {info.last}
</h2>
<h2>Email: {info.email}</h2>
<h2>Company: {info.company}</h2>
<h2>Skill: {info.skill}</h2>
<h2>Average: {info.average}</h2>
}
</div>
);
}
export default function StudentList() {
const [students, setStudents] = useState([]);
Check React's documentation React.
Also check if it cannot be declared that way, you need to use a ternary if. Check that part in React's documenation Conditional rending
{students.map(function (student, index) {
index <= 25 && (
<div className="col" key={index}>
<StudentInfo data={student} />
</div>
);

ReactJS context-api won't render after I get data

This is a next.js site, since both my Navbar component and my cart page should have access to my cart's content I created a context for them. If I try to render the page, I get:
Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'key')
obs: The cartContent array exists and has length 1, I can get it by delaying when the data's rendered by using setTimeout, but, can't get it to render right after it's fetched.
I need to make it render after the data from firebase is returned, but always met with the mentioned error.
This is my _app.tsx file
function MyApp({ Component, pageProps }) {
// set user for context
const userContext = startContext();
return (
<UserContext.Provider value = { userContext }>
<Navbar />
<Component {...pageProps} />
<Toaster />
</UserContext.Provider>
);
}
export default MyApp
This file has the startContext function that returns the context so it can be used.
export const startContext = () => {
const [user] = useAuthState(auth);
const [cart, setCart] = useState(null);
const [cartContent, setCartContent] = useState(null);
useEffect(() => {
if (!user) {
setCart(null);
setCartContent(null);
}
else {
getCart(user, setCart, setCartContent);
}
}, [user]);
return { user, cart, setCart, cartContent, setCartContent };
}
This file contains the getCart function.
export const getCart = async (user, setCart, setCartContent) => {
if (user) {
try {
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
if (new_cart) {
let new_cartContent = []
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
console.log(new_cartContent);
setCartContent(new_cartContent);
console.log(new_cartContent);
setCart(new_cart);
}
else {
setCart(null);
setCartContent(null);
}
} catch (err) {
console.error(err);
}
}
}
This is the cart.tsx webpage. When I load it I get the mentioned error.
export default () => {
const { user, cart, cartContent } = useContext(UserContext);
return (
<AuthCheck>
<div className="grid grid-cols-1 gap-4">
{cartContent && cartContent[0].key}
</div>
</AuthCheck>
)
}
I've tried to render the cart's content[0].key in many different ways, but couldn't do it. Always get error as if it were undefined. Doing a setTimeout hack works, but, I really wanted to solve this in a decent manner so it's at least error proof in the sense of not depending on firebase's response time/internet latency.
Edit:
Since it works with setTimeout, it feels like a race condition where if setCartContent is used, it triggers the rerender but setCartContent can't finish before stuff is rendered so it will consider the state cartContent as undefined and won't trigger again later.
Try changing
{cartContent && cartContent[0].key}
to
{cartContent?.length > 0 && cartContent[0].key}
Edit:: The actual problem is in getCart function in line
let new_cart = await (await getDoc(doc(firestore, 'carts', user.uid))).data();
This is either set to an empty array or an empty object. So try changing your if (new_cart) condition to
if (Object.keys(new_cart).length > 0) {
Now you wont get the undefined error
Since there seemed to be a race condition, I figured the setCartContent was executing before its content was fetched. So I changed in the getCart function the map loop with an async function for a for loop
await Object.keys(new_cart).map(async (key) => {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
});
to
for (const key of Object.keys(new_cart)) {
new_cartContent.push({...(await getDoc(doc(firestore, 'products-cart', key))).data(), key: key});
}
I can't make a map function with await in it without making it asynchronous so I the for loop made it work. Hope someone finds some alternatives to solving this, I could only come up with a for loop so the code is synchronous.

map not a function using react hooks

i'm trying to populate a select bar with a name from an API call. I Have created my hook, also useEffect for its side effects, and passed the data down the return. its giving me map is not a function error. my variable is an empty array but the setter of the variable is not assigning the value to my variable. How can i clear the map not a function error ? i have attached my snippet. Thanks.
import React, { useEffect, useState } from "react";
import axios from "axios";
const Sidebar = () => {
const [ingredients, setIngredients] = useState([]);
useEffect(() => {
const fetchIngredients = async (url) => {
try {
let res = await axios.get(url);
setIngredients(res.data);
} catch (error) {
setIngredients([]);
console.log(error);
}
};
fetchIngredients(
"https://www.thecocktaildb.com/api/json/v2/1/search.php?i=vodka"
);
}, []);
const displayIngredients = ingredients.map((ingredient) => {
setIngredients(ingredient.name);
return <option key={ingredient.name}>{ingredients}</option>;
});
return (
<div className="sidebar">
<label>
By ingredient:
<select>{displayIngredients}</select>
</label>
</div>
);
};
export default Sidebar
First, here
setIngredients(res.data);
change res.data to res.ingredients (the response object doesn't have data property). Then you'll face another bug,
const displayIngredients = ingredients.map((ingredient) => {
setIngredients(ingredient.name);
//...
First, ingredient.name is undefined, and second, it probably would be a string if it existed. Just ditch the setIngredients call here.
You are declaring displayIngredients as a variable typeof array (By directly affecting the array.map() result). You need it to be a function that return an array as follow :
const displayIngredients = () => ingredients.map((ingredient) => {
// Do not erase your previous values here
setIngredients(previousState => [...previousState, ingredient.name]);
// Changed it here as well, seems more logic to me
return <option key={ingredient.name}>{ingredient.name}</option>;
});
You should also wait for the API call to end before to display your select to prevent a blank result while your data load (If there is a lot). The easiest way to do that is returning a loader while the API call is running :
if(!ingredients.length) {
return <Loader />; // Or whatever you want
}
return (
<div className="sidebar">
<label>
By ingredient:
<select>{displayIngredients}</select>
</label>
</div>
);

Needs Help To Troubleshoot Fetching Single Document From Firebase Database As Detailed Page

I'm try to get single document as detail information from Firebase database under collection "books", however my array method map does not recognize as function due to the render produce "undefined". Somehow render again and produce the object value in log. I posted the screenshot of the log above, hoping somebody help me out, thanks!!!!!
import React, {useState, useEffect} from 'react'
import {Link} from 'react-router-dom'
import firebase from '../config/fbConfig'
const BookDetails = (props) => {
const [books, setBooks] = useState([])
useEffect(() => {
const db = firebase.firestore()
const id = props.match.params.id
var docRef = db.collection("books").doc(id);
docRef.get().then(doc => {
if(doc.exists){
const data = doc.data()
console.log("Document data:", data)
setBooks(data)
}else {
console.log("No such document!");
}
}).catch(error => {
console.log("Error getting document:", error);
})
}, [])
console.log('this log is before return', books.title)
return (
<div className="book_details">
<Link to="/"><h2>Home</h2></Link>
{console.log("this log is in the return method", books.title)}
<h1>The Summary Of the Book </h1>
{books.map( book => <ul key = "book.id" >
<li>Book Title: {book.title}</li>
<li>Book Author: {book.author}</li>
<li>Book Summery: {book.brief}</li>
</ul>)}
</div>
)
}
export default BookDetails
Because you are testing whether books is undefined and only call the map function if it is defined (i.e. {books && books.map( [...] )}), the problem must lie somewhere else.
You are fetching a single document from your Firebase database. Therefore, the returned data will not be an array but an object which does not have the map function in its prototype. You can verify this from your console logs.
Your component renders twice because you are changing its state inside the useEffect via setBooks(data).
const db = firebase.firestore()
const id = props.match.params.id
First of all move these lines inside of useEffect.
Coming to the problem
You are fetching a single doc(object) from firebase and saving it in a state which is an array. Change your useState to
const \[book, setBook\] = useState(undefined) // or useState({})
Change your return to
return (
<div className="book_details">
<Link to="/"><h2>Home</h2></Link>
{console.log("this log is in the return method", books.title)}
<h1>The Summary Of the Book </h1>
{book && <div key={book.id}> {book.brief} </div>}
</div>
)
// or {Object.keys(book).length !== 0 && <div key={book.id}> {book.brief} </div>}
if you have used empty object in useState.

Resources