I have a parent component
// generate an array of objects, which gets passed the the CalendarDateGrid Component
const calculatedCalendarData = (startDate, endDate) => {
return [
{ calcNormalOrderElectoralBoard: calcNormalOrderElectoralBoard(endDate) },
{ calcNormalDiploma: calcNormalDiploma(startDate) },
];
};
export default function Home() {
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const [selectedRegion, setSelectedRegion] = useState(
questions[2].selections[0]
);
const [electoralProcess, setElectoralProcess] = useState(
questions[3].selections[0]
);
const [loading, setLoading] = useState(false);
const [calendarData, setCalendarData] = useState(calculatedCalendarData(startDate, endDate));
return (
<div>
<main className="font-heebo">
<div className="max-w-full mx-auto">
<Calendar
startDate={startDate}
onDateChange={setStartDate}
endDate={endDate}
onEndDateChange={setEndDate}
selectedRegion={selectedRegion}
setSelectedRegion={setSelectedRegion}
electoralProcess={electoralProcess}
setElectoralProcess={setElectoralProcess}
calendarData={calendarData}
setCalendarData={setCalendarData}
setLoading={setLoading}
/>
</div>
<div className="max-w-full mx-auto">
<CalendarDateGrid
data={calendarData}
setData={calculatedCalendarData}
isLoading={loading}
/>
</div>
</main>
</div>
);
}
Inside my Calendar component (which is a form), I am saving the data and passing that data to my parent
<div className="py-14">
<div
onClick={() => setCalendarData}
className="cursor-pointer bg-white w-72 mx-auto text-center text-wahl-red rounded"
>
<span className="inline-block border-wahl-red rotate-180 border w-5 align-middle" />
<span type="button" className="inline-block px-3 py-2 text-lg">
calculate date
</span>
</div>
</div>
After that I am trying to pass that data to my CalendarDateGrid component.
Which basically shall take the data and generate a layout mapping through that data:
export default function CalendarDateGrid({ data, isLoading }) {
const [calendarData, setCalendarData] = useState(data);
useEffect(() => {
setCalendarData(data);
}, [data]);
return (
<div className="w-4/5 2xl:w-2/3 mx-auto pb-20">
{isLoading && (
<ul
role="list"
className="mt-3 grid grid-cols-1 gap-5 sm:gap-12 md:grid-cols-2 xl:grid-cols-3"
>
{calendarData.map((calendarItem) => (
...
The issues is that my child component does not update if the data do get updated. I am trying to update using useEffect but that does not work.
The useEffect hook implement a shallow comparison on the value passed as a dependency. So If you want to pass an Object or an Array as your dependency, you should define what it exactly is.
Object Dependecy
For example, if you have an object as your state, you should define which property to be considered as a dependency:
const [user, setUser] = useState({
name: '',
age: ''
})
// If you want to run the useEffect on every changes of user
useEffect(() => {
console.log("a property in object is changing")
}, [...user])
// If you want to run the useEffect only on change of user's age
useEffect(() => {
console.log("user's age is changing")
}, [user.age])
Array Dependecy
const [data, setData] = useState([])
// If event that cause change in data, affect on its length
useEffect(() => {
console.log("array's length is changing")
}, [data.length])
If the data's length does not change, or data is an array of objects that inner fields may change, you can use one of the following ways:
Comparing the data value with its previous value by using JSON.stringify(data)
const [data, setData] = useState([])
useEffect(() => {
console.log("array is changing")
}, [JSON.stringify(data)])
You can use use-deep-compare-effect package, or write your own custom hook if you what exactly is the properties inside of the nested objects of data array.
Related
Hello I am working on a shopping cart project, where user can add/subtract quantity depending on the button clicked . The problem I am having is if I move state up to the parent component the itemQty is the same for all entities. So I moved it down to child component and that keeps the states separate for each product. This works up until I need to access that itemQty to calculate the subtotal.
I have tried passing a function from parent to child to return the itemQty but it ended up returning itemQty for each product.
I have tried useRef, but I don't think it is ment for this situation and I ended up with some error.
Any help will be very appreciated.
Parent Component
export default function cart() {
let total = useContext(getTotalContext)
let setTotal = useContext(setTotalContext)
const [products, setProducts] = useState([])
const [sum, setSum] = useState(0)
/* const prices = products.map((x) => x.price).reduce((a, b) => a + b, 0) // use to calculate total price */
const prices = products.map((x) => x.price).reduce((a, b) => a + b, 0)
const itemQtyRef = useRef(null)
useEffect(() => {
// localStorage.clear();
setProducts(JSON.parse(localStorage.getItem("products"))) // get add to cart data initially to create cart
localStorage.getItem('count') === 0 ? setTotal(JSON.parse(localStorage.getItem("products")).length) : setTotal(localStorage.getItem('count')) // return length if no count total
}, [])
useEffect(() => {
localStorage.setItem('count', total) // stores total for navbar after every change in dropdown etc
}, [total])
useEffect(() => { // upload changes to products to storage every change
localStorage.setItem("products", JSON.stringify(products))
setSum(prices) //
console.log(products)
}, [products])
return (
<div className="Page-Container">
<div className="Product-Container">
{products.map((product, i) => {
return (
<div key={i}>
<Image
className=""
alt="Image Unavailable"
src={product.image}
width={300}
height={300} />
<h4 className="text-sm text-gray-700">{product.title}</h4>
<h5 className="text-lg font-medium ">${product.price}</h5>
<h6 className="no-underline hover:no-underline">{product.rate}/5 of {product.count} Reviews</h6> {/*Add stars to */}
<button className="bg-black-500 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded-full"
onClick={() => { //remove product on click of x
setProducts(products.filter((x) => x.id !== product.id))//filters out by product id clicked
setTotal(total - 1)
// setTotal(total-product.itemQty) // removed item qty from total
}}>x</button>
<QtyButton product={product} setTotal={setTotal} total={total} />
</div>
)
})}
Child Component
export default function QtyButton(props) {
const [itemQty, setItemQty] = useState(1)
return (
<div>
<button className="bg-green-500 hover:bg-gray-400 font-bold py-2 px-4"
onClick={() => {
setItemQty(itemQty + 1)
}}>+</button>
<button className="bg-red-500 hover:bg-gray-400 font-bold py-2 px-4" onClick={() => {
if (itemQty > 0) {
setItemQty(itemQty - 1)
}
}}>-</button><div>Quantity: {itemQty}</div>
</div>
)
}
Lifting the state to the parent component is the right choice here.
But you seem to have gotten a bit of tunnel vision with the state. You should instead send in functions to modify parent state and have another state object which hold "purchase product data", indexed on product.id
// at the top
const [purchases, setPurchases] = useState({})
// in the state initializing useEffect
const _products = JSON.parse(localStorage.getItem("products"))
setProducts(_products)
setPurchases(_products.reduce((acc,p) => ({ ...acc, [p.id]:
{quantity:0}}),{}))
// in returning render
<QtyButton onPlus={() => setPurchases(purchases => {...purchases, purchases[product.id]: { quantity: purchases[product.id]++}})}
// onMinus={}
/>
first of all, it's not the good way.means calling a child function from parent .Normally what we do is passing the function to the child component from parent.means you can define the function in parent and pass it to child
but if you must need like this, please use ref.
wrap up the child function(which u need to call from parent) with forwardRef and useImperativeHandle
for example
const QtyButton = forwardRef((props, ref) => {
const [itemQty, setItemQty] = useState(1)
useImperativeHandle(ref, () => ({
handleqty(qty) {
setItemQty(itemQty)
}
}));
return (
<div>
<button className="bg-green-500 hover:bg-gray-400 font-bold py-2 px-4"
onClick={() => {
setItemQty(itemQty + 1)
}}>+</button>
<button className="bg-red-500 hover:bg-gray-400 font-bold py-2 px-4" onClick={() => {
if (itemQty > 0) {
setItemQty(itemQty - 1)
}
}}>-</button><div>Quantity: {itemQty}</div>
</div>
)
});
in parent
const qtyButtonRef = useRef();
<QtyButton ref={qtyButtonRef} product={product} setTotal={setTotal} total={total} />
and call the child function by using
qtyButtonRef.current.handleqty();
I am taking a React course in which week by week we have certain challenges that lead us to create an E-Commerce.
My problem is that, I have the data of a product hardcoded, when entering the page useEffect creates a promise that is resolved in 2 seconds using setTimeOut and returns the product data.
In a previous challenge I already did essentially the same thing, only having an array with several products and following the design pattern of: ItemListContainer asks for the data, passes the data to ItemList, applies .map() to the array and for each item creates an Item component sending by props the data.
In the current challenge as we are working with a single product we do not have to do .map() on any array, and this for some reason causes the Item component (in this case called ItemDetail) to render before the data arrives, although it only renders the parts that do not depend on the data arriving to it.
Demo: Demo (It renders the styled div and the "$" sign).
After several hours looking at the code I can't figure out why it happens and what I could do to fix it.
Github repo: enzom-uy/coderhouseECommerce
ItemDetailContainer code:
import React, { useState, useEffect } from 'react'
import ItemDetail from '../ItemDetail/ItemDetail'
const productData = {
id: '4',
name: 'Cuarto producto',
description: 'Descripcion del cuarto producto',
price: 10,
pictureUrl:
'https://media.istockphoto.com/vectors/thumbnail-image-vector-graphic-vector-id1147544807?k=20&m=1147544807&s=612x612&w=0&h=pBhz1dkwsCMq37Udtp9sfxbjaMl27JUapoyYpQm0anc='
}
const ItemDetailContainer = () => {
const [detailedProduct, setDetailedProduct] = useState({})
useEffect(() => {
const fetchingData = new Promise((res, rej) => {
setTimeout(() => {
res(productData)
}, 2000)
})
fetchingData.then((res) => {
setDetailedProduct(res)
console.log('Se guardaron los datos')
})
fetchingData.catch((err) => {
console.log('Failed')
})
}, [])
return (
<>
<h1>Producto detallado</h1>
<ItemDetail product={detailedProduct} />
</>
)
}
export default ItemDetailContainer
ItemDetail code:
import React from 'react'
export default function ItemDetail({ product }) {
return (
<div className="bg-slate-100 w-60 flex flex-col items-center mx-1 px-2 border border-slate-400 text-center">
<span>{product.name}</span>
<img src={product.pictureUrl} width="120" />
<p>{product.description}</p>
<span>${product.price}</span>
</div>
)
}
The Short Answer
It is showing the styled div and the dollar sign because that is what you have included in the JSX. To remove it until the data arrives, you can make it a part of the return statement conditionally on the data being present. That might look something like this (using the simpler of the options listed later in my answer)
return (
<>
<h1>Producto detallado</h1>
{detailedProduct.name && <ItemDetail product={detailedProduct} />}
</>
)
Why It's Happening
If you have a simple component that doesn't rely on data, then it will render everything in the return statement. In the below example, that is the div, h1, and p element.
function Parent() {
return (
<div>
<h1>Hello!</h1>
<p>We have no products to sell.</p>
</div>
);
}
If you have a component that does rely on data, say from an API (or a setTimeout), then that doesn't change. What is included in the return statement (including any child components) will be rendered.
function Parent() {
const [data, setData] = useState({});
useEffect(() => {
fetch('https://api.com')
.then(res => res.json())
.then(setData)
.catch(err => console.error(err));
}, []);
return (
<div>
<h1>Hello!</h1>
<Child data={data} />
</div>
);
}
function Child({ data }) {
return (
<p>The name is: {data.name}</p>
);
}
In this case, before the data comes back from the API, the child element's <p> element with the text "The name is: " will appear, since it is a part of the return. Then, when the data arrives, it will re-render and the name will appear. If we want it to say something else, we can do that with something like this...
function Child({ data }) {
return (
<p>{data.name ? `The name is: ${data.name}` : "Loading name..."}</p>
);
}
This will ask the component, "is there a value for the data.name property? If yes, then display the text "The name is: ___" where the name is filled in the blank. If no, then display the text "Loading name..."
This is also option 1 for your solution. You can use a ternary operator to provide a default value when the data has not yet arrived.
Another option is to just remove the entire component that relies on the data. Since the return statement will only return items that are truth-y, you can conditionally render your ItemDetail component. Our example might look something like this...
function Parent() {
const [data, setData] = useState({});
useEffect(() => {
fetch('https://api.com')
.then(res => res.json())
.then(setData)
.catch(err => console.error(err));
}, []);
return (
<div>
<h1>Hello!</h1>
{data.name && <Child data={data} />}
</div>
);
}
function Child({ data }) {
return (
<p>The name is: {data.name}</p>
);
}
This is option 2 for your solution. It is quick and easy, but there are distinct differences between how they impact developer and user experience.
As an option 3, you could also implement a loading component that would take the place of the product component (including something like a spinner or progress bar), but I think it may be more work than your React course calls for.
Important Note. While the second option is definitely easier since you don't have to code default values for every field, it can sometimes be worse for user experience, since entire components will appear and disappear when data loads in, which can cause big layout shifts.
You can do a few things like:
Don't render your component until the request is done or...
Use loading flag state
Use an extra state for the "loading" flag.. you can start this on "true" and after finished change it to false and render your component with the data.
This is my example:
import React, { useState, useEffect } from "react";
const productData = {
id: "4",
name: "Cuarto producto",
description: "Descripcion del cuarto producto",
price: 10,
pictureUrl:
"https://media.istockphoto.com/vectors/thumbnail-image-vector-graphic-vector-id1147544807?k=20&m=1147544807&s=612x612&w=0&h=pBhz1dkwsCMq37Udtp9sfxbjaMl27JUapoyYpQm0anc="
};
const ItemDetailContainer = () => {
const [detailedProduct, setDetailedProduct] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchingData = new Promise((res, rej) => {
setTimeout(() => {
res(productData);
}, 2000);
});
fetchingData.then((res) => {
setDetailedProduct(res);
setLoading(false);
console.log("Se guardaron los datos");
});
fetchingData.catch((err) => {
console.log("Failed");
});
}, []);
if (loading) return "Cargando...";
return (
<>
<h1>Producto detallado</h1>
<ItemDetail product={detailedProduct} />
</>
);
};
export default ItemDetailContainer;
function ItemDetail({ product }) {
return (
<div className="bg-slate-100 w-60 flex flex-col items-center mx-1 px-2 border border-slate-400 text-center">
<span>{product.name}</span>
<img src={product.pictureUrl} width="120" />
<p>{product.description}</p>
<span>${product.price}</span>
</div>
);
}
i have an array, called reportsData, then i need to filter it, generating some checkboxes with each of them having a label based on each name that comes from another array (emittersData), so basically i set it like this:
const [searchUser, setSearchUser] = useState<string[]>([])
const mappedAndFiltered = reportsData
.filter((value: any) =>
searchUser.length > 0 ? searchUser.includes(value.user.name) : true
)
Then i render my checkboxes like this:
function EmittersCheckboxes () {
const [checkedState, setCheckedState] = useState(
new Array(emittersData.length).fill(false)
)
const handleOnChange = (position: any, label: any) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
)
setSearchUser((prev) =>
prev.some((item) => item === label)
? prev.filter((item) => item !== label)
: [...prev, label]
)
setCheckedState(updatedCheckedState)
};
return (
<div className="App">
{emittersData.map((value: any, index: any) => {
return (
<li key={index}>
<div className="toppings-list-item">
<div className="left-section">
<input
className="h-4 w-4 focus:bg-indigo border-2 border-gray-300 rounded"
type="checkbox"
id={`custom-checkbox-${index}`}
name={value.Attributes[2].Value}
value={value.Attributes[2].Value}
checked={checkedState[index]}
onChange={() => handleOnChange(index, value.Attributes[2].Value)}
/>
<label className="ml-3 font-medium text-sm text-gray-700 dark:text-primary" htmlFor={`custom-checkbox-${index}`}>{value.Attributes[2].Value}</label>
</div>
</div>
</li>
);
})}
</div>
)
}
And on the react component i am rendering each checkbox, that is a li, like:
<ul><EmittersCheckboxes /></ul>
And i render the mappedAndFiltered on the end.
Then it is fine, when i click each generated checkbox, it filters the array setting the state in setSearch user and the array is filtered.
You can check it here: streamable. com /v6bpk6
See that the filter is working, the total number of items in the array is changing based on the checkbox selected (one or more).
But the thing is that each checkbox does not become 'checked', it remains blank (untoggled).
What am i doing wrong, why doesnt it check itself?
You've defined your EmittersCheckboxes component inside another component. and every time that the parent component renders (by state change) your internal component is redefined, again and again causing it to lose it's internal state that React holds for you.
Here's a simplified example:
import React, { useState } from "react";
function CheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>CheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
export default function App() {
const [counter, setCounter] = useState(1);
function InternalCheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>InternalCheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
return (
<>
<InternalCheckboxeComponent />
<CheckboxeComponent />
<button onClick={() => setCounter((c) => c + 1)}>{counter}</button>
</>
);
}
There's the App (parent component) with its own state (counter), with a button to change this state, clicking this button will increase the counter, causing a re-render of App. This re-render redefines a new Component named InternalCheckboxeComponent every render.
The InternalCheckboxeComponent also has an internal state (checkedState).
And there's an externally defined functional component named CheckboxeComponent, with this component React is able to hold its own state, because it's not redefined (It's the same function)
If you set the state of each to be "checked" and click the button, this will cause a re-render of App, this will redefine the InternalCheckboxeComponent function, causing React to lose its state. and the CheckboxeComponent state remains in React as it's the same function.
I am learning React and I was trying to develop a code to fetch random 10 numbers with corresponding interesting facts from an api "Numbers API".
the issue I am facing is that, when I run the code, an error appears
"Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead"
below I have attached the code:
const originalArray = new Array(10).fill(0);
const mappedArray = originalArray.map((n, index) =>
Math.floor(Math.random() * 100)
);
console.log("mappedArray", mappedArray);
console.log("joined array string", mappedArray.join(","));
async function callApi(numbers) {
const fetchResponse = await fetch(`http://numbersapi.com/${numbers}/math`);
if (fetchResponse.ok) {
const data = await fetchResponse.json();
console.log("all good, heres the response", data);
} else console.log("There was a problem");
}
const Number = async() => {
const [add, setAdd] = React.useState([]); // <-- valid initial empty state
React.useEffect(() => {
callApi(mappedArray).
then(values => setAdd(values));
}, []); // <-- invoke on component mount
<div className="container">
{add.map((newlist, i) => (
<div className="item" key={i}>
{newlist}
</div>
))}
</div>
);
export default Number;
Can someone help me fix this issue? Thank you
You need to maintain state. React is very particular with it's lifecycle. So here I have a data state, and a function - setData - that updates it.
For React function components we use useEffect - essentially the old componentDidMount from class components - to load data when the component first mounts. We can then set the state with that data which we now know is an array of objects.
Once the state updates the component gets re-rendered. You can then iterate over the Object.entries using the key for the key, and the value as the text source.
function Number() {
// Initialise the state with an empty array
const [data, setData] = useState([]);
// Separate out the function that creates the number array
function createNumbers() {
const originalArray = new Array(10).fill(0);
return originalArray.map(() => Math.floor(Math.random() * 100)).join('');
}
useEffect(() => {
async function getNumbers() {
try {
const res = await fetch(`http://numbersapi.com/${createNumbers()}/math`);
const data = await res.json();
// Set the new component state using the data
setData(data);
} catch (err) {
console.log(err);
}
}
getNumbers();
}, []);
return (
<div className="container">
{Object.entries(data).map(([key, value]) => (
<div className="item" key={key}>
{value}
</div>
))}
</div>
);
};
export default Number;
callApi is async so it implicitly returns a Promise, which you save into add and attempt to render. This is the object that React is complaining about.
You will want to invoke this function within the React component lifecycle and save the result into state to be rendered.
const Number = () => {
const [add, setAdd] = React.useState([]); // <-- valid initial empty state
React.useEffect(() => {
callApi(mappedArray).
then(values => setAdd(values));
}, []); // <-- invoke on component mount
return (
<div className="container">
{add.map((newlist, i) => (
<div className="item" key={i}>
{newlist}
</div>
))}
</div>
);
};
When you send a comma separated list of numbers to this numbersapi it returns a JSON object that isn't renderable as an array.
The response format will always be a JSON map from numbers to facts,
of at most 100 numbers.
Example response to http://numbersapi.com/53,65,70/math
{
"53": "53 is the 16thprime number.",
"65": "65 is the 23rdsemiprime and the 3rd of the form (5.q)it is an octagonal number.",
"70": "70 is the smallest weird number."
}
It seems you want to either save the values into state:
React.useEffect(() => {
callApi(mappedArray).
then(result => setAdd(Object.values(result)));
}, []);
Or you can get the array of values when rendering:
return (
<div className="container">
{Object.values(add).map((newlist, i) => (
<div className="item" key={i}>
{newlist}
</div>
))}
</div>
);
If you want both the key and the value, then use Object.entries to get an array of array of key value pairs.
Example:
[
['53', '53 is the 16thprime number.'],
['65', '65 is the 23rdsemiprime and the 3rd of the form (5.q)it is an octagonal number.'],
['70', '70 is the smallest weird number.'],
]
return (
<div className="container">
{Object.entries(add).map(([key, value], i) => (
<div className="item" key={key}>
{key}: {value}
</div>
))}
</div>
);
That's because your add function is still return a Promise since it an async function
You will still need to await for add to finished, then use it:
Something like this:
const Number = async () => {
const data = await add;
<div className="container">
{data.map((newlist, i) => (
<div className="item" key={i}>
{newlist}
</div>
))}
</div>;
};
I'm working on a basic Todo-app and currently implementing a progress bar to showcase how many of the added tasks have been completed.
The way I've done it is that I'm using the following method, which is called every time there is a change in my Task object. This is done with React useEffect and is working like a charm when it comes to the output of that console.log.
import {useEffect} from "react";
const Daily = ({ tasks, numberOfTasks, tasksCompleted }) => {
// Updating progress with useEffect every time something happens with tasks
useEffect(() => {
updateProgressHandler();
}, [tasks]);
// Check and update progress based on completed tasks
const updateProgressHandler = () => {
numberOfTasks = tasks.length;
tasksCompleted = tasks.filter(task => task.completed === true).length;
console.log(`Updating progress for ${numberOfTasks} tasks, where ${tasksCompleted} are completed.`);
}
// Saving the completed % to a variable
const completed = numberOfTasks / tasksCompleted * 10;
//console.log(numberOfTasks);
return (
<div className="row">
<div className="col">
<div className="progress progress-daily" style={{ height: "38px" }}>
<div
className="progress-bar progress-bar-striped progress-bar-animated bg-success"
role="progressbar"
aria-valuenow={tasksCompleted}
aria-valuemin="0"
aria-valuemax={numberOfTasks}
style={{width: `${completed}%`}}
>
{completed}%
</div>
</div>
</div>
<div className="col-1">
<button className="btn btn-success">Done</button>
</div>
</div>
)
}
export default Daily;
My issue here however, is the fact that I can't figure out how to pass the numberOfTasks and tasksCompleted to my Daily progress object.
State is set within the App.js as follows:
function App() {
// State
const [numberOfTasks, tasksCompleted] = useState(0);
const [inputText, setInputText] = useState("");
const [tasks, setTasks] = useState([]);
return ( ... );
}
So, while it works perfectly fine in the console.log, the value I'm getting in the progress bar itself is NaN. How come?