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>
Related
I am trying to set the title of my document to "/{orderNumber}orders" and for that orderNumber value to update every time user clicks the button and different number of orders from filtered array are displayed on the screen.
For context, I am importing a json file, filtering it to display the correct elements I want decided by user input, and I am then calculating the length of that array, of which that integer needs to be stored in orderNumber variable to update document title.
I know I am accessing the correct value of the arrays as I have console logged it, my issue is how to update this state change every re render without this error throwing: (Uncaught ReferenceError: Cannot access 'ordersToDisplay' before initialization)
Code:
import { Col, Row } from "antd";
import { useContext, useEffect, useMemo, useState } from "react";
import Order from "../components/Order";
import { AppContext } from "../context/Context";
import AntButton from "../elements/Button";
import ordersToDisplay from "../orders.json";
const OrdersPage = () => {
const [filteringStatus, setFilteringStatus] = useState("");
const {orderAmount, setOrderAmount} = useContext(AppContext)
const [test, setTest] = useState("")
const setDiplayAcceptedOrders = () => {
setFilteringStatus("accepted");
setTest(ordersToDisplay.length)
};
const setDiplayCompletedOrders = () => {
setFilteringStatus("complete");
setTest(ordersToDisplay.length)
};
const setDiplayInProgressOrders = () => {
setFilteringStatus("inProgress");
setTest(ordersToDisplay.length)
};
const ordersToDisplay = useMemo(() => {
if (filteringStatus) {
return ordersToDisplay.filter((i) => i.orderStatus === filteringStatus);
}
return ordersToDisplay;
}, [filteringStatus]);
console.log("Orders to display: ", ordersToDisplay);
console.log("test value: ", test)
return(
<div className="container">
<Row justify="space-between" align="middle">
<Col span={6}>
<h1>Orders</h1>
</Col>
<Col span={6}>
<AntButton onClick={setDiplayAcceptedOrders} name="Accepted"/>
</Col>
<Col span={6}>
<AntButton onClick={setDiplayInProgressOrders} name="In Progress"/>
</Col>
<Col span={6}>
<AntButton onClick={setDiplayCompletedOrders} name="Complete"/>
</Col>
</Row>
<Row>
<Col span={12}>
<h3>{filteringStatus == "" ? "All Orders" : filteringStatus}</h3>
{ordersToDisplay.map((e) => {
return(
<Order
key={e.id}
productName={e.productName}
dateOrdered={e.dateOrdered}
orderStatus={e.orderStatus}
/>
)
})}
</Col>
</Row>
</div>
)
}
export default OrdersPage;
app
const App = () => {
const [orderAmount, setOrderAmount] = useState("")
const Routes = useRoutes([
{path: "/", element: <HomePage/>},
{path: `/(${orderAmount})orders`, element: <OrdersPage/>}
])
return (
<AppContext.Provider value={{
orderAmount, setOrderAmount
}}>
<div>
{Routes}
</div>
</AppContext.Provider>
);
};
export default App;
You are masking the imported ordersToDisplay with what you are trying to memoize. Rename the memoized version/variable. You need only store in state the current filteringStatus state, the test state seems unnecessary and isn't used from what I see.
To update the orderAmount state in the context, use a useEffect hook with a dependency on the computed/memoized orders value to issue a side-effect to update the orderAmount value.
Example:
import { Col, Row } from "antd";
import { useContext, useEffect, useMemo, useState } from "react";
import Order from "../components/Order";
import { AppContext } from "../context/Context";
import AntButton from "../elements/Button";
import ordersToDisplay from "../orders.json";
const OrdersPage = () => {
const [filteringStatus, setFilteringStatus] = useState("");
const { orderAmount, setOrderAmount } = useContext(AppContext);
const setDiplayAcceptedOrders = () => {
setFilteringStatus("accepted");
};
const setDiplayCompletedOrders = () => {
setFilteringStatus("complete");
};
const setDiplayInProgressOrders = () => {
setFilteringStatus("inProgress");
};
// rename to something else, anything but ordersToDisplay
const orders = useMemo(() => {
if (filteringStatus) {
return ordersToDisplay.filter((i) => i.orderStatus === filteringStatus);
}
return ordersToDisplay;
}, [filteringStatus]);
useEffect(() => {
console.log("Orders to display: ", orders); // <-- output derived value
// update amount when orders array updates
setOrderAmount(orders.length);
}, [orders, setOrderAmount]);
return (
<div className="container">
<Row justify="space-between" align="middle">
<Col span={6}>
<h1>Orders</h1>
</Col>
<Col span={6}>
<AntButton onClick={setDiplayAcceptedOrders} name="Accepted"/>
</Col>
<Col span={6}>
<AntButton onClick={setDiplayInProgressOrders} name="In Progress"/>
</Col>
<Col span={6}>
<AntButton onClick={setDiplayCompletedOrders} name="Complete"/>
</Col>
</Row>
<Row>
<Col span={12}>
<h3>{filteringStatus == "" ? "All Orders" : filteringStatus}</h3>
{orders.map((e) => { // <-- use here
return (
<Order
key={e.id}
productName={e.productName}
dateOrdered={e.dateOrdered}
orderStatus={e.orderStatus}
/>
)
})}
</Col>
</Row>
</div>
);
};
export default OrdersPage;
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.
I have a datagrid table in which I'm getting my database data from an API call and I have written the table code in one file. I also have a search functionality where you can search for a particular record inside the table, but this search code is in another file. I am having difficulty of passing my state variable containing the search parameter from my search file to the table file. I have separated all my components in different pages since it'd be easier to structure them using a grid in my App.js. How do I get my search query to my table file next?
My search code:
export default function SearchInput() {
const [searchTerm, setSearchTerm] = React.useState('');
return (
<Grid item xs={3}>
<Box mt={1.6}
component="form"
sx={{
'& > :not(style)': { m: 1, width: '20ch', backgroundColor: "white", borderRadius: 1},
}}
noValidate
autoComplete="off"
>
<TextField
placeholder="Search Customer ID"
variant="outlined"
size="small"
sx={{input: {textAlign: "left"}}}
onChange={(event) => {
setSearchTerm(event.target.value);
console.log(searchTerm);
}}
/>
</Box>
</Grid>
);
}
My table code:
export default function DataTable() {
const [pageSize, setPageSize] = React.useState(10);
const [data, setData] = React.useState([]);
useEffect(async () => {
setData(await getData());
}, [])
return (
<div style={{ width: '100%' }}>
<DataGrid
rows={data}
columns={columns}
checkboxSelection={true}
autoHeight={true}
density='compact'
rowHeight='40'
headerHeight={80}
disableColumnMenu={true}
disableSelectionOnClick={true}
sx={datagridSx}
pageSize={pageSize}
onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
rowsPerPageOptions={[5, 10, 15]}
pagination
/>
</div>
);
}
App.js
function App() {
return (
<div className="App">
<Container maxWidth="false" disableGutters="true">
<Grid container spacing={0}>
<ABClogo />
<HHHlogo />
</Grid>
<Grid container spacing={0}>
<LeftButtonGroup />
<SearchInput />
<RightButtonGroup />
</Grid>
<Grid container spacing={0}>
<DataTable />
<TableFooter />
</Grid>
</Container>
</div>
);
}
Here is a minimal example using createContext(), and useReducer() to lift up state and share it between components, similar to what you are after, but as jsNoob says, there are multiple options. This is one I'm comfortable with.
The concept is explained here: https://reactjs.org/docs/context.html
Essentially you can create 'global' state at any point in your component tree and using Provider / Consumer components, share that state and functionality with child components.
//Main.js
import React, { createContext, useContext, useReducer } from 'react';
const MainContext = createContext();
export const useMainContext => {
return useContext(MainContext);
}
const mainReducer = (state, action) => {
switch(action.type){
case: 'SOMETHING':{
return({...state, something: action.data});
}
default:
return state;
}
}
export const Main = () => {
const [mainState, mainDispatch] = useReducer(mainReducer, {something: false});
const stateOfMain = { mainState, mainDispatch };
return(
<MainContext.Provider value={stateOfMain}>
<MainContext.Consumer>
{() => (
<div>
<Nothing />
<Whatever />
</div>
)}
</MainContext.Consumer>
</MainContext.Provider>
)
}
Then you can have the other components use either or both of the main state and dispatch.
//Nothing.js
import {mainContext} from './Main.js'
const Nothing = () => {
const { mainState, mainDispatch } = useMainContext();
return(
<button onClick={() => {mainDispatch({type: 'SOMETHING', data: !mainState.something})}}></button>
)
}
Clicking the button in the above file, should change the display of the below file
//Whatever.js
import {mainContext} from './Main.js'
const Whatever = () => {
const { mainState } = useMainContext();
return(
<div>{mainState.something}</div>
);
}
const Section = ({ text, changeFn }) => {
return(
<>
<Content text={text} changeFn={changeFn}/>
<Content text={text} changeFn={changeFn}/>
<Content text={text} changeFn={changeFn}/>
</>
)
}
const Content = ({ changeFn, text }) => {
const [ textCont, setText ] =useState('text')
return(
<>
<div>{textCont}</div>
<button onClick={changeFn}>Edit</button>
</>
)
}
const Form = ({ changeFn, setContent }) => {
let inputRef = useRef()
const applyChangefn = () => {
setContent(inputRef.current.value)
changeFn()
}
return(
<>
<input ref={inputRef}/>
<button onClick={applyChangefn}>Save</button>
</>
)
}
I want to make a component which exists as example with its own usestate in Content component.
Then i want to send it multiplied by 3 times into Section component.
First question is how to send props between sibling components
Second question is how to make 3 same components which have its own useState state. I mean 3 same components with independent usestate.
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;