I have two components that I'm rendering based on the condition of a state, but I'm running into a problem where the wrong component is displayed a split second before the right component is displayed.
Fetching data async:
const [test, setTest] = useState();
const [loading, setLoading] = useState();
const [error, setError] = useState();
const fetchData = async () => {
console.log("running");
setLoading(true);
setError(false);
try {
const result = await axios(
"https://jsonplaceholder.typicode.com/posts/props.selectedId" // Is dynamic and changes on user click
);
setTest(result.data);
} catch (error) {
if (error.response.status == 404) {
setError(error);
setTest(null);
}
}
setLoading(false);
};
Rendering:
return (
<div className={classes.root}>
{!loading && !error && test? (
<div>
<Card className={classes.card}>
<CardContent>
<Title>Adattributes</Title>
<Typography variant="h6" component="h1">
name
</Typography>
<Grid container spacing={3}>
<Grid item xs={3}>
<Typography variant="subtitle2" component="h1">
address
</Typography>
{test.title}
</Grid>
</Grid>
</CardContent>
<CardActions>
<Component1
value={test}
setTest={setTest}
/>
</CardActions>
</Card>
</div>
) : (
<Component2 setTest={setTest} />
)}
</div>
);
});
Am I doing something wrong with the conditional rendering? Or do it have something to do with fetching async?
The initial state of your test state is falsy. This, plus operator precedence can lead to wrong errors. See http://www-lia.deis.unibo.it/materiale/JS/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence.html.
Probably what you want to so is:
(!loading && !error && test) ? ... : ...
In the first line you use useState hook without initial state, so your state become undefined (remember that undefined is falsy value).
const [test, setTest] = useState();
Either you should set initial state for test, or change your condition for rendering component.
Related
In a React app, I am trying to create a custom alert in a separate component as shown below:
employee.ts:*
const [open, setOpen] = useState(false);
const [severity, setSeverity] = useState("");
const [message, setMessage] = useState("");
<CustomAlert open={open} severity={severity} message={message} />
custom-alert.js:
export default function CustomAlert(props) {
const{open, message, severity} = props;
return (
<Snackbar open={open} autoHideDuration={6000} >
{severity !== null && severity !== undefined && severity !== "" ? (
<Alert
variant="filled"
onClick={() => {
setOpen(false);
}}
severity={severity}
sx={{ width: "100%" }}
>
{message}
</Alert>
) : (
<div></div>
)}
</Snackbar>
)
}
Although it works on the first call, it cannot be displayed for the next call from employee component. So, should I define some listener etc? Or can I fix the problem easily by using a smart approach?
I'm trying to fetch the shop and it's coupons, I have two model one for the shop and one for the coupon, also two routers, one for fetching shops and one for fetching coupons, the shops are fetching fine and showing in client side, but the coupons are not showing in the client side. When /coupons/${shopName} I try it in postman it works fine, but in the client side not, I don't know why. Console log is giving me [object Object]
export default function ShopPage() {
const [shop, setShop] = useState("");
const shopName = useParams().shopName;
const [coupons, setCoupons] = useState([]);
useEffect(() => {
const fetchShop = async () => {
const res = await axios.get(`/shops/${shopName}`);
setShop(res.data);
console.log(res.data);
};
fetchShop();
}, [shopName]);
useEffect(() => {
const fetchShopCoupons = async () => {
const response = await axios.get(`/coupons/${shopName}`);
setCoupons(response.data);
console.log("Shop Coupons are:" + response.data);
};
fetchShopCoupons();
}, []);
return (
<>
<Box>
<Stack>
<Stack >
<Avatar alt={(shop.shopName)}
src={shop.shopPic}/>
<Stack>
<Box>
<Typography>
{shop.shopName}
</Typography>
</Box>
</Box>
</Stack>
</Stack>
<Box>
<Coupons coupons={coupons}/>
</Box>
</Stack>
</Box>
</>
)
}
Coupons Component:
export default function Coupons({ coupons = [] }) {
const [filteredResults, setFilteredResults] = useState([]);
const [searchInput, setSearchInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const filter = (e) => {
const keyword = e.target.value;
if (keyword !== '') {
const filteredData = coupons.filter((coupon) => {
return Object.values(coupon)
.join('')
.toLowerCase()
.includes(searchInput.toLowerCase())
})
setFilteredResults(filteredData)
} else {
setFilteredResults(coupons);
}
setSearchInput(keyword);
}
console.log("filtered Coupons are:", filteredResults);
return (
<div className="coupons">
<div className="couponsContainer">
<div className="couponsSearchContainer">
<div className="couponsSearch">
<div class="couponsSearchIconContainer">
<SearchIcon class="w-5 h-5" />
</div>
<input type="text"
className="couponsSearchInput"
placeholder="بحث"
name="couponSearchText"
id="couponSearchText"
onChange={filter}
/>
</div>
{/* ENDS OF COUPONSSEARCHCONTAINER */}
</div>
{/* ENDS OF COUPONSSEARCH */}
<div className="couponsBox">
{isLoading ? (
<Box sx={{ display: 'flex' }}>
<CircularProgress />
</Box>
) : (
filteredResults.length > 0 ? (
filteredResults.map((f) => (
<Coupon coupon={f} />
))
) : (
coupons.sort((a, b) =>
new Date(b.createdAt) - new Date(a.createdAt))
.map((c) => (
<Coupon coupon={c} />
)))
)
}
</div>
{/* ENDS OF COUPONSBOX */}
</div>
{/* ENDS OF COUPONSCONTAINER */}
</div>
//ENDS OF COUPONS
);
}
Maybe you could try response.data.value or response.data.value[0], its works for me.
The problem can be with useEffect() you are using for fetching "shopCpupons". The dependency array of useEffect which fetches "shopCoupons" is empty('[]'), which means that that useEffect will immediately call the fetchShopCoupons() function as soon as the page loads, but at that time you will not have the "shopName" which is the endpoint you need in the fetchShopCoupons API call:
const response = await axios.get(/coupons/$**{shopName}**);
1st Solution) So the solution is that in the 2nd useEffect which fetches shop coupons you can add "shopname" as dependency in dependency array, just like you did it in 1st useEffect which fetches shopdetails, The code can look like this
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
export default function ShopPage() {
const [shop, setShop] = useState("");
const shopName = useParams().shopName;
const [coupons, setCoupons] = useState([]);
useEffect(() => {
const fetchShop = async () => {
const res = await axios.get(`/shops/${shopName}`);
setShop(res.data);
console.log(res.data);
};
fetchShop();
}, [shopName]);
useEffect(() => {
const fetchShopCoupons = async () => {
const response = await axios.get(`/coupons/${shopName}`);
setCoupons(response.data);
console.log("Shop Coupons are:" + response.data);
};
fetchShopCoupons();
}, [shopName]);
return (
<>
<Box>
<Stack>
<Stack >
<Avatar alt={(shop.shopName)}
src={shop.shopPic}/>
<Stack>
<Box>
<Typography>
{shop.shopName}
</Typography>
</Box>
</Box>
</Stack>
</Stack>
<Box>
<Coupons coupons={coupons}/>
</Box>
</Stack>
</Box>
</>
)
}
Here I added 'shopname' as dependency in 2nd useEffect.
2nd Solution: or else you can use only one useEffect and control your shop and coupons variables like this
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
export default function ShopPage() {
const [shop, setShop] = useState("");
const shopName = useParams().shopName;
const [coupons, setCoupons] = useState([]);
useEffect(() => {
const fetchShop = async () => {
const res = await axios.get(`/shops/${shopName}`);
setShop(res.data);
console.log(res.data);
};
const fetchShopCoupons = async () => {
const response = await axios.get(`/coupons/${shopName}`);
setCoupons(response.data);
console.log("Shop Coupons are:" + response.data);
};
fetchShop();
fetchShopCoupons();
}, [shopName]);
return (
<>
<Box>
<Stack>
<Stack >
<Avatar alt={(shop.shopName)}
src={shop.shopPic}/>
<Stack>
<Box>
<Typography>
{shop.shopName}
</Typography>
</Box>
</Box>
</Stack>
</Stack>
<Box>
<Coupons coupons={coupons}/>
</Box>
</Stack>
</Box>
</>
)
}
Here as you can see I merged 2 useEffects into one useEffect as both your APIs are dependent upon "shopname" dependency, so once you will get shopname from params the program will first call the fetchShop() function which will fetch shop details and after that it will call fetchCoupons() which will call coupons for the shop. You can also rearrange the order of these function calls as per the requirements of project. This approach will make you code leaner and cleaner. However you can choose to use the best fit solutions out of 2 as per the requirement of the project.
Okay there is definitely a quick solution for this I just can't figure out.
Just a description of what I am trying to do:
Whenever I hover over a certain card, I would like to see the description of that item and only that item. But instead what's obviously happening, as you can see from my code, is every single cards description is showing.
I rewrote a simpler version of the code by taking out any unnecessary pieces. Everything is imported correctly, styling and classNames were removed as well.
export function Items() {
const [items, setItems] = useState([])
const [isHovering, setIsHovering] = useState(false)
useEffect(() => {
setItems(Data)
}, [])
function handleMouseOver() {
setIsHovering(true)
}
function handleMouseOut() {
setIsHovering(false)
}
return(
<div>
{items.map(item => {
return(
<Card onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} key={item.id}>
{isHovering ?
<Card.Body>
<p>{item.item_description}</p>
</Card.Body>
:
<Card.Body>
</Card.Body>
}
<Card.Footer>
</Card.Footer>
</Card>
)
})}
</div>
)
}
As far as I can see you don't need to put this logic into parent component, and also it makes everything more complex, since it's hard to manage hovering. I would create new chlid component and manage this state out there internally.
export function Item({item}) {
const [isHovering, setIsHovering] = useState(false);
useEffect(() => {
setItems(Data);
}, []);
function handleMouseOver() {
setIsHovering(true);
}
function handleMouseOut() {
setIsHovering(false);
}
return (
<Card onMouseOver={handleMouseOver} onMouseOut={handleMouseOut}>
{isHovering ? (
<Card.Body>
<p>{item.item_description}</p>
</Card.Body>
) : (
<Card.Body></Card.Body>
)}
<Card.Footer></Card.Footer>
</Card>
);
}
export function Items() {
const [items, setItems] = useState([]);
return (
<div>
{items.map(item => (
<Item key={item.id} item={item} />
))}
</div>
);
}
Your "isHovering" state should also be an array, where you store the hover state for every card. Then on hover set "isHovering" to true only for the right card.
On page load I'm getting the weather data for an api and then displaying it on sidebar then when you click on a city it shows the the city's weather in more detail. So basically I've been passing the data around from parent to child with props. I need to fill the detailed component with some initial data so I'm trying to send the first object in the data array to the child component through props and set it to state but when I try to render it is undefined and I'm not sure why.
It actually seems to be coming back undefined a couple times before setting it but when I try to render it on the page {weather.data.temp} I get 'Cannot read property 'temp' of undefined'.
Parent:
const fetchCity = async (city) => {
const res = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`);
return {
description: res.data.weather[0].description,
icon: res.data.weather[0].icon,
temp: res.data.main.temp,
city: res.data.name,
country: res.data.sys.country,
id: res.data.id,
};
};
function App() {
const [data, setData] = useState([]);
const [activeWeather, setActiveWeather] = useState([]);
useEffect(() => {
const fetchCities = async () => {
const citiesData = await Promise.all(["Ottawa", "Toronto", "Vancouver", "California", "London"].map(fetchCity)).catch((err) => {
console.log(err);
});
setData((prevState) => prevState.concat(citiesData));
};
fetchCities();
}, []);
const handleClick = (event) => {
const weather = JSON.parse(event.target.dataset.value);
setActiveWeather(weather);
};
return (
<div className="App">
<Header />
<Container>
<Row>
<Col>
<WeatherPanel data={data} handleClick={handleClick} />
</Col>
<Col>
<ActiveWeather activeWeather={activeWeather} data={data[0]} />
</Col>
</Row>
</Container>
</div>
);
}
export default App;
Child
import React, { useEffect, useState } from "react";
import { Container, Card } from "react-bootstrap";
const ActiveWeather = (props) => {
const [weather, setWeather] = useState();
useEffect(() => {
setWeather(props.data);
}, [props]);
console.log(weather);
return (
<Container>
<Card>
<Card.Header> </Card.Header>
{weather.temp}
</Card>
</Container>
);
};
export default ActiveWeather;
Other child
const WeatherPanel = (props) => {
return (
<div>
<Container fluid>
<Card style={{ boxShadow: "0 0 10px 2px lightgrey" }}>
<Card.Header> Favorite Location</Card.Header>
<ListGroup variant="flush">
<ListGroup.Item>
{props.data.map((item) => (
<ListGroup.Item key={item.id} data-value={JSON.stringify(item)} onClick={props.handleClick}>
<img src={`http://openweathermap.org/img/wn/${item.icon}#2x.png`} alt="Weather Icon" />
{item.city + ", " + item.country}
</ListGroup.Item>
))}
</ListGroup.Item>
</ListGroup>
</Card>
</Container>
</div>
);
};
export default WeatherPanel;
Issue
Calling setState does not update the state variable immediately. Why?
Solution
Check if weather is defined before accessing it.
Option 1: Use weather?.temp
Option 2: Use an if statement: if (!weather)
I think the issue is with your useEffect in the ActiveWeather component.
Because you are passing just props as the dependancy of your useEffect, any prop change will trigger it. So in your example activeWeather is probably triggering it, and you are then setting the state of weather to undefined because at mount stage, there isn't any data to pass in.
If you limit the dependence to only props.data. Then that useEffect will only run if something gets passed though the data prop. You can even use an if statement in there to double check that there is actually some data.
useEffect(() => {
if(props.data){
setWeather(props.data);
}
}, [props.data]);
In your parent component you are also passing in data[0] as the data prop. On mount, data is an empty array so if you say data[0] it will be undefined. Maybe wrap the rendering of your ActiveWeather component in an if to check if there is any data
<Col>
{ data.length > 0 && <ActiveWeather activeWeather={activeWeather} data={data[0]} /> }
</Col>
I have been writing an application (e-Commerce, as a project, following a tutorial) with React. I am getting an error of 'TypeError: Cannot read property 'length' of undefined' when referring to a cart object. Here's some background context. I am generating the cart object using a useState hook near the top of my App.js component:
const [cart, setCart] = useState({});
A little further down in App.js a console.log statement executes without errors suggesting that the cart object exists:
console.log(cart);
However, when I try to pass the cart object to a Cart component in the render section of App.js the aforementioned error (e.g. 'TypeError: Cannot read property 'length' of undefined') is generated. Why is this happening and how do I fix it?
Here is the code of App.js
import React, { useState, useEffect } from 'react'
import { commerce } from './lib/commerce';
import { Products, Navbar, Cart } from './components';
const App = () => {
const [products, setProducts] = useState([]);
const [cart, setCart] = useState({});
const fetchProducts = async () => {
const { data } = await commerce.products.list();
setProducts(data);
}
const fetchCart = async () => {
setCart(await commerce.cart.retrieve());
}
const handleAddToCart = async (productID, quantity) => {
const item = await commerce.cart.add(productID, quantity);
setCart(item.cart);
}
useEffect(() => {
fetchProducts();
fetchCart();
}, []);
console.log(cart);
return (
<div>
<Navbar totalItems={cart.total_items} />
{/* <Products products={products} onAddToCart={handleAddToCart} /> */}
<Cart cart={cart} />
</div>
)
}
export default App
And here is the code of the component (Cart) that I am passing the cart object into:
import React from 'react'
import { Container, Typography, Button, Grid } from "#material-ui/core";
import useStyles from './styles';
const Cart = ({ cart }) => {
const isEmpty = !cart.line_items.length;
const classes = useStyles();
const EmptyCart = () => {
<Typography variant="subtitle1">
You have no items your shopping cart..
</Typography>
}
const FilledCart = () => {
<>
<Grid container spacing={3}>
{
cart.line_items.map((item) => (
<Grid item xs={12} sm={4} key={item.id}>
<div>{item.name}</div>
</Grid>
))
}
</Grid>
<div className={classes.cardDetails}>
<Typography variant="h4">
Subtotal: { cart.subtotal.formatted_with_symbol }
</Typography>
<div>
<Button className={classes.emptyButton} size="large" variant="contained" type="button" color="secondary">Empty Cart</Button>
<Button className={classes.checkoutButton} size="large" variant="contained" type="button" color="primary">Checkout</Button>
</div>
</div>
</>
}
return (
<Container>
<div className={classes.toolbar} />
<Typography className={classes.title} variant="h3">Your shopping cart</Typography>
{
isEmpty ? <EmptyCart /> : <FilledCart />
}
</Container>
)
}
export default Cart
Issue
The issue with your code is that the initial state doesn't match what you access on the initial render.
In App component the cart state is just an empty object.
const [cart, setCart] = useState({});
cart is passed as a prop to Cart component and the code assumes cart.line_items is defined in order to access a length property or map function. cart.line_items is OFC undefined so attempting to access the length property (or map) throws the TypeError: Cannot read property 'XYZ' of undefined
const isEmpty = !cart.line_items.length;
and
cart.line_items.map(.....
but when I console.log it out in App.js, it actually does print out
the necessary information.
The console.log(cart); is in the function body of the component so it's incorrectly logging the cart state as an unintentional side-effect, it should be logged from a useEffect hook so you see the value per render cycle. The other issue here is that you aren't accessing any nested properties so this will never throw an error. I'd be willing to bet that with this code you have at least 1 or 2 logs entires that are just empty objects ({}) and then you see some logs with populated nested properties.
Example possible log output:
{}
{}
{ line_items: [.....], subtotal: ..... }
{ line_items: [.....], subtotal: ..... }
Solutions
Regarding the state logging, you should use an useEffect hook with a dependency on the state value you are logging. This will log the cart state on the initial render and later only when the cart state value is updated.
useEffect(() => {
console.log(cart);
}, [cart]);
For the error, you've several options to help guard against errors when accessing your cart state.
Provide valid initial state that matches what is accessed during the render cycle, add line_items: [] to the initial cart state such that cart.line_items will now exist and have a length property.
const [cart, setCart] = useState({ line_items: [] });
Use a guard clause or Optional chaining on the passed cart prop.
const isEmpty = !(cart.line_items && cart.line_items.length);
or
const isEmpty = !cart.line_items?.length);
and
cart.line_items && cart.line_items.map(.....
or
cart.line_items?.map(.....
May as well guard the subtotal sub-state as well in the case that cart.subtotal isn't defined.
<Typography variant="h4">
Subtotal: { cart.subtotal?.formatted_with_symbol }
</Typography>
There is another way to approach the error (it solved my problem).
We only need to add this line:
if (!cart.line_items) return "Loading...";
and then remove the variable from the top and add it inside of the if statement:
{!cart.line_items.length ? <EmptyCart /> : <FilledCart />}
as sometimes we don't need to create a variable if the content is meaningful enough on its own.
The code:
import React, { useEffect } from "react";
import { Container, Typography, Button, Grid } from "#material-ui/core";
import useStyles from "./styles";
//
//
const Cart = ({ cart }) => {
// const isEmpty = !(cart.line_items && cart.line_items.length);
const classes = useStyles();
//
//So if the cart is EMPTY show the following:
const EmptyCart = () => {
<Typography variant="subtitle1">
You have no items in your shopping cart, start adding some!
</Typography>;
};
//
//
//So if the cart is FILLED show the following:
const FilledCart = () => (
<>
<Grid container spacing={3}>
{cart.line_items.map((item) => (
<Grid item xs={12} sm={4} key={item.id}>
<div>{item.name}</div>
</Grid>
))}
</Grid>
<div className={classes.cardDetails}>
<Typography variant="h4">
Subtotal: {cart.subtotal?.formatted_with_symbol}
</Typography>
<div>
<Button
className={classes.emptyButton}
size="large"
type="button"
variant="contained"
color="secondary"
>
Empty cart
</Button>
<Button
className={classes.checkoutButton}
size="large"
type="button"
variant="contained"
color="primary"
>
Checkout
</Button>
</div>
</div>
</>
);
//
if (!cart.line_items) return "Loading";
return (
<Container>
<div className={classes.toolbar} />
<Typography className={classes.title} variant="h3">
Your shopping Cart
</Typography>
{!cart.line_items.length ? <EmptyCart /> : <FilledCart />}
</Container>
);
};
export default Cart;