How do I access a child components state in React.js - javascript

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();

Related

If state one already has a value I want to set state two instead

I have a deck of cards with assigned numbers using dataset-keyMatch.
When I click on the first card I want it to assign the keyMatch value to the first state (choiceOne) and then when I click a second card I want to check that choiceOne has a value and if that is true I then assign choiceTwo the keyMatch value of the second card.
The card elements are imported from a MatchingCard Component.
Then the onClick event is assigned in the [classId] Component using the function handleChoice.
For choiceOne & choiceTwo
I tried with a default state of null
Then I tried with a default state of 0
// -----------------Matching Card Game Functionality --------------------
// --------------------------------------------------------------------------
// States
const [doubledDeck, setDoubledDeck] = useState([]);
const [shuffledDeck, setShuffledDeck] = useState([]);
const [deckReady, setDeckReady] = useState([]);
const [matchingGameActive, setMatchingGameActive] = useState(false);
const [turns, setTurns] = useState(0);
const [choiceOne, setChoiceOne] = useState(0);
const [choiceTwo, setChoiceTwo] = useState(0);
// test the logic that I want for the onClick event on the MatchingCard
// This works....so I don't know why it won't work when I click the cards
const random = Math.floor(Math.random() * 10);
const check = () => {
choiceOne != 0 ? setChoiceTwo(random) : setChoiceOne(random);
if (turns === 0) {
setChoiceOne(random);
setTurns((prevTurn) => prevTurn + 1);
}
if (turns === 1) {
setChoiceTwo(random);
setTurns(0);
}
};
// Take the original double sided deck - split it - and combine into big deck for game
const doubleTheDeck = (deck: any) => {
const FlashcardsEnglish = deck.map((card: { props: { children: { props: any }[] } }) => {
return card.props.children[0].props;
});
const FlashcardsJapanese = deck.map((card: { props: { children: { props: any }[] } }) => {
return card.props.children[1].props;
});
const joinedDeck = FlashcardsEnglish.concat(FlashcardsJapanese);
setDoubledDeck(joinedDeck);
};
// shuffle deck -----
const shuffle = (deck: any[]) => {
const shuffledCards = deck
.sort(() => Math.random() - 0.5)
.map((card) => ({
...card,
}));
setShuffledDeck(shuffledCards);
};
// choice functionality
const handleChoice = (e: { target: { dataset: { keyMatch: any } } }) => {
const parsed = parseInt(e.target.dataset.keyMatch);
// const str = e.target.dataset.keyMatch;
// attempt #1 using "null" as the default state for choiceOne and Two
// choiceOne ? setChoiceTwo(parsed) : setChoiceOne(parsed);
// attempt #2 using 0 as the default state for choiceOne and Two
if (turns === 0) {
setChoiceOne(parsed);
setTurns((prevTurn) => prevTurn + 1);
}
if (turns === 1) {
setChoiceTwo(parsed);
setTurns(0);
}
};
console.log(choiceOne, choiceTwo);
// create JSX elements------
const finalDeck = shuffledDeck.map((card: { matchId: any; word: any }) => {
const { matchId, word } = card;
return (
<MatchingCards key={matchId + word[0]} matchId={matchId} word={word} handleChoice={handleChoice}></MatchingCards>
);
});
// prepare deck for game start -----
const handleMatchingGameClick = () => {
// take flahscards and double split them into two - doubling size
doubleTheDeck(cardsForMatchingGame);
// shuffle the deck & sets the shuffledCards
shuffle(doubledDeck);
// create JSX elements from the new cards
setDeckReady(finalDeck);
// set game to active
setMatchingGameActive((prevState) => !prevState);
};
// useEffect(() => {}, []);
return (
<div>
<div>
<Header pageHeader={className} />
<div className="bg-white">choice 1: {choiceOne}</div>
<div className="bg-white">choice 2: {choiceTwo}</div>
<button onClick={check}>check</button>
</div>
{/* <ToggleButton /> */}
<div className="flex items-center justify-between bg-slate-200 dark:bg-bd-1 p-4 ">
<HomeButton />
{/* <button onClick={handleMatchingGameClick}>start game</button> */}
{/* <ShuffleButton onClick={shuffle(doubledDeck)} /> */}
<MatchingGameButton
content={matchingGameActive ? "Back to Regular Deck" : "Go to Matching Game"}
onClick={handleMatchingGameClick}
/>
<ToggleButton />
</div>
<div
className="
dark:bg-bd-1
p-10
bg-slate-200 gap-5 flex flex-col items-center justify-center
sm:items-center sm:justify-center
sm:grid
sm:grid-cols-2
md:grid
md:grid-cols-3
lg:grid-cols-4
"
>
{matchingGameActive ? deckReady : cards};
</div>
</div>
);
What I return from the MatchingCards Component
return (
<div>
<div>
<Header pageHeader={className} />
<div className="bg-white">choice 1: {choiceOne}</div>
<div className="bg-white">choice 2: {choiceTwo}</div>
<button onClick={check}>check</button>
</div>
{/* <ToggleButton /> */}
<div className="flex items-center justify-between bg-slate-200 dark:bg-bd-1 p-4 ">
<HomeButton />
{/* <button onClick={handleMatchingGameClick}>start game</button> */}
{/* <ShuffleButton onClick={shuffle(doubledDeck)} /> */}
<MatchingGameButton
content={matchingGameActive ? "Back to Regular Deck" : "Go to Matching Game"}
onClick={handleMatchingGameClick}
/>
<ToggleButton />
</div>
<div
className="
dark:bg-bd-1
p-10
bg-slate-200 gap-5 flex flex-col items-center justify-center
sm:items-center sm:justify-center
sm:grid
sm:grid-cols-2
md:grid
md:grid-cols-3
lg:grid-cols-4
"
>
{matchingGameActive ? deckReady : cards};
</div>
</div>
);
When I click on a card my function only assigns choiceOne and then continues to reassign choiceOne. ChoiceTwo never gets given a value despite ChoiceOne already having a value.
I made a test function called "check" and the logic works there so I have no idea why it is not working on my card element.
When a component renders MatchingCards Component is called leads to new state creation.

React Child component does not rerender on prop change

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.

I have in issue with filter function in my react ecommerce app

React filter function is working
When I click on the filter option like "Denim", "shirt" etc it should filter the products and show only those items only. I used the handleTag function to filter the array of products and then update the setfilterProducts but it is not working.
Below my main file Home.jsx
function Home() {
const [products, setProducts] = useState([]);
const [filterProducts, setFileterProducts] = useState([]);
const [sortValue, setSortValue] = useState('');
const [tag, setTag] = useState('');
// const [error, setError] = useState('');
const loadAllProducts = () => {
/*var myArray = ['a', 1, 'a', 2, '1'];
let unique = [...new Set(myArray)];
console.log(unique); // unique is ['a', 1, 2, '1']*/
let ar = data.map(item => item.tag);
let uniqueEle = [...new Set(ar)];
// console.log(uniqueEle);
setProducts(data);
setFileterProducts(data);
}
useEffect(() => {
loadAllProducts();
handleSort();
handleTag();
}, [filterProducts, sortValue, tag])
const handleSort = (value) => {
setSortValue(value);
listProducts();
}
function listProducts() {
if(sortValue !== '') {
// console.log("SORT: "+sortValue)
products.sort((a, b) => sortValue === 'lowest' ? (parseInt(a.price) > parseInt(b.price) ? 1:-1) : (parseInt(a.price) < parseInt(b.price) ? 1:-1))
} else {
// console.log("INSIDE THE ELSE PART");
products.sort((a, b) => (a.id > b.id ? 1 : -1))
}
return {setFileterProducts: products}
}
const handleTag = (value) => {
// console.log(value);
setTag(value);
listTagProduct();
}
function listTagProduct() {
if(tag !== '') {
if(tag === 'Denim') {
console.log(tag);
let filterAr = tagFilter(tag);
console.log("FilterArr "+filterAr);
return {setFileterProducts: filterAr}
} else if(tag === "T-shirt") {
let filterAr = tagFilter(tag);
return {setFileterProducts: filterAr}
} else if(tag === "shirt") {
let filterAr = tagFilter(tag);
return {setFileterProducts: filterAr}
} else if(tag === "jacket") {
let filterAr = tagFilter(tag);
return {setFileterProducts: filterAr}
}
}
return {setFileterProducts: products}
}
// utility function
function tagFilter(tag) {
let ar = filterProducts.filter(item => item.tag === "Denim");
// console.log(ar);
return ar;
}
return (
<div className = "container-fluid">
<h2>All Products: {filterProducts.length}</h2>
<Filter
product = {filterProducts}
handleSort = {handleSort}
sort = {sortValue}
handleTag = {handleTag}
tag = {tag}
/>
<hr />
<div className="row custom-card">
{filterProducts.map((product, idx) => {
console.log(product);
return(
<div key = {product.id} className="col-md-3 mb-2">
<Card products = {product}/>
</div>
)
})}
</div>
</div>
)
}
export default Home
This uses to display the filter options Filter.jsx
function Filter(props) {
const handleChange = (event) => {
// console.log(event.target.value);
return props.handleSort(event.target.value);
}
const handleTagChange = (event) => {
// console.log(event.target.value);
return props.handleTag(event.target.value);
}
let tagNames = ["T-shirt", "shirt", "Denim", "jacket"];
return (
<div className="container-fluid">
<div className="row">
<div className="col-12 col-md-9">
<p className = " mr-2">Filters:<span className="mr-4"></span>
{tagNames.map((item, idx) => (
<span key = {idx} >
<button
type="button"
class="btn btn-outline-secondary mr-2 custom-filter"
value= {item}
onClick = {handleTagChange}
>{item}</button>
</span>
))}
</p>
</div>
<div className="col-12 col-md-3">
<select className="form-control" value = {props.sort} onChange = {handleChange}>
{/* {console.log("CHECKING: "+props.sort)} */}
<option value="">Sort by</option>
<option value="lowest">lowest to highest</option>
<option value="highest">highest to lowest</option>
</select>
</div>
</div>
</div>
)
}
export default Filter
codesandbox link
Your Filter.jsx looks alright and should work fine. I only made a few simple changes like moving the onClick event handlers inline, and added the selected button some styles to distinguish its state.
Mutating the state directly
On the other hand, your Home.jsx looks overcomplicated, so I needed to clean things up. One of the biggest mistakes was mutating the states directly. It can lead to really odd bugs. You probably didn't even noticed that you changed the state, but products.sort for example mutates the original products array.
You should clone the array first (you can use .slice() or the spread operator), sort the clone, and update the state with this new sorted array.
Weird return statements
Another problem is returning objects like {setFileterProducts: products}, or {setFileterProducts: filterAr}. I'm not sure what you wanted to do, maybe it meant to be setFileterProducts(filterAr)?
The useEffect hooks
The next problem is the usage of the useEffect hook. You tried to loadAllProducts every time a filter tag, a sort by value, or the filtered products change which is absolutely unnecessary.
You could instead use two useEffects; one to load the data from the API and set the products and filteredProducts states, and another one to update filteredProducts every time the tag or sort value changes. You don't need anything else because whenever the filteredProducts array changes, React re-renders the component.
Some more simplification
I changed a few variable names and I tried to remove every unnecessary lines from Home.jsx; only the states, the two useEffect hooks, and the return statement stayed. Filter and sort is handled by the hook, there is no need to use additional functions for these.
I also simplified the sort functions. If you want to sort numbers, you just need to subtract them (and as subtraction changes the price's type to a number, you don't even need to use parseInt anymore).
Home.jsx
import React, { useEffect, useState } from "react";
import data from "../../../data.json";
import Card from "../card/Card.jsx";
import Filter from "../filter/Filter.jsx";
import "./Home.css";
function Home() {
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
const [sortBy, setSortBy] = useState("");
const [selectedTag, setSelectedTag] = useState("");
useEffect(() => {
setProducts(data);
setFilteredProducts(data);
}, []);
useEffect(() => {
const filtered = selectedTag
? products.filter((item) => item.tag === selectedTag)
: products;
setFilteredProducts(
sortBy
? [...filtered].sort((a, b) =>
sortBy === "lowest" ? a.price - b.price : b.price - a.price
)
: [...filtered].sort((a, b) => (a.id > b.id ? 1 : -1))
);
}, [selectedTag, sortBy, products]);
return (
<div className="container-fluid">
<h2>Products: {filteredProducts.length}</h2>
<Filter
handleSort={setSortBy}
handleTagChange={setSelectedTag}
selectedTag={selectedTag}
sortBy={sortBy}
/>
<hr />
<div className="row custom-card">
{filteredProducts.map((product) => (
<div key={product.id} className="col-md-3 mb-2">
<Card products={product} />
</div>
))}
</div>
</div>
);
}
export default Home;
Filter.jsx:
import React from "react";
import "./Filter.css";
function Filter({ handleSort, handleTagChange, selectedTag, sortBy }) {
let tagNames = ["T-shirt", "shirt", "Denim", "jacket"];
return (
<div className="container-fluid">
<div className="row">
<div className="col-12 col-md-9">
<p className=" mr-2">
Filters: <span className="mr-4"></span>
{tagNames.map((tag, idx) => (
<button
key={idx}
type="button"
className={`btn mr-2 custom-filter ${
selectedTag === tag
? "btn-secondary"
: "btn-outline-secondary"
}`}
onClick={(e) => handleTagChange(e.target.value)}
value={tag}
>
{tag}
</button>
))}
</p>
</div>
<div className="col-12 col-md-3">
<select
className="form-control"
onChange={(e) => handleSort(e.target.value)}
value={sortBy}
>
<option value="">Sort by</option>
<option value="lowest">Lowest to highest</option>
<option value="highest">Highest to lowest</option>
</select>
</div>
</div>
</div>
);
}
export default Filter;
CodeSandbox link

How can I lift up the state from Child to Parent in my React app?

I need to lift up the state of my Child component to the Parent to be able to reset this within the resetTotals() function. Each child component within the map has a click counter, so this needs to be reset within the parent component onClick.
How can I pass up the state?
// Parent Functional Component
function Parent() {
const [calorieCount, setCalorieCount] = useState(0);
const [vitaminACount, setVitaminACount] = useState(0);
const [vitaminDCount, setVitaminDCount] = useState(0);
function updateTotals(calories = 0, vitaminA = 0, vitaminD = 0) {
setCalorieCount(prevCalorieCount => Math.round(prevCalorieCount + calories));
setVitaminACount(prevVitaminACount => Math.round((prevVitaminACount + vitaminA) * 100) / 100);
setVitaminDCount(prevVitaminDCount => Math.round((prevVitaminDCount + vitaminD) * 100) / 100);
}
function resetTotals() {
setCalorieCount(0);
setVitaminACount(0);
setVitaminDCount(0);
}
return (
<div className="App">
<main className="products-grid flex flex-wrap">
{FoodCards.map((item, i) => {
return <Child
key={item.id}
name={item.name}
img={item.img}
calories={item.calories}
vitamin_a={item.vitamin_a}
vitamin_d={item.vitamin_d}
updateTotals={updateTotals} />
})}
</main>
<footer>
<div
className="reset"
onClick={() => resetTotals()}
>Reset</div>
</footer>
</div>
);
}
export default App
// Child Functional Component
const Child = (props) => {
const [clickCount, setClickCount] = useState(0);
function handleUpdateTotals(calories, vitamin_a, vitamin_d) {
props.updateTotals(calories, vitamin_a, vitamin_d);
setClickCount(prevClickCount => prevClickCount + 1);
}
return (
<div
className="product"
onClick={() => handleUpdateTotals(props.calories, props.vitamin_a, props.vitamin_d)}
>
<p>{props.name}</p>
<p>{clickCount > 0 ? <p>Selected: {clickCount}</p> : <p>Not Selected</p>}</p>
<img src={props.img} alt="" />
</div>
);
}
You are already updating the parent state from the child in that code.
You are passing in a callback function as a property, then calling it by props.updateTotals(). That will then run the updateTotals function in parent.
Do the same for reset totals: pass the method in as a prop, and call it from the child.

Change value of object property with checkbox React

I am pretty new to react and got really stuck on something. I am working on a sort of ordering application. People can order a product and can select all ingredients they want. I was thinking to do this with a checkbox for each ingredient. Unfort. I just don't know how to get this fixed. Also, I am wondering if I have to use a state in my component or just a variable.
So I am mapping through the array of ingredients and for each ingredient I am displaying a checkbox to turn on/off an ingredient. So my main question, How can I adjust my object with these checkboxes, and if I need to have a state in my component to keep up to date with the checkboxes, How will I set the product to my state? Because It's coming from props.
I've tried all sort of things, for instance this from the docs:
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
But then again, How can I add the product to my state? Also, this will be different since the ingredients are in a object, and the example from the docs are just values and not in a specific object.
My component
import React from 'react';
import { Link } from 'react-router-dom';
class Order extends React.Component {
constructor(props) {
super(props);
}
handleToggle(e) {
//Handle the toggle, set the value of the ingredient to 0 or 1
}
getData(e, product) {
e.preventDefault();
console.log(product)
}
render() {
const product = this.props.products.find((product) => {
return product.id == this.props.match.params.id;
});
return (
<form className="container mx-auto px-4 pt-6" onSubmit={(e) => this.getData(e, product) }>
<Link to={`/${this.props.match.params.category}`} className="mb-4 relative block text-brand hover:text-brand-dark">← Terug naar categorie</Link>
<div className="flex flex-row items-center justify-between bg-white rounded px-8 py-8 shadow-sm mb-4">
<div className="">
<h2 className="text-brand uppercase">{product && product.name}</h2>
<div className="ingre">
<p>
{product && product.ingredients.map((item) => {
return <span className="ing text-grey-dark text-sm" key={item.name}>{item.name}</span>
})}
</p>
</div>
</div>
<div className="">
<h3 className="text-brand text-4xl">€{product && product.price}</h3>
</div>
</div>
<div className="flex flex-wrap mb-4 -mx-2">
{product && product.ingredients.map((item) => {
return (
<div className="w-1/2 mb-4 px-2" key={item.name}>
<div className="flex flex-row items-center justify-between bg-white rounded px-8 py-8 shadow-sm">
<div>
<h3 className="text-grey-dark font-normal text-sm">{item.name}</h3>
</div>
<div>
<input type="checkbox" checked={item.value} name={item} onChange={(e) => this.handleToggle(e)}/>
</div>
</div>
</div>
);
})}
</div>
<button type="submit" className="bg-brand hover:bg-brand-dark text-white font-bold py-4 px-4 rounded">
Order this product
</button>
</form>
);
}
}
export default Order;
An example of a product
So actually I need to keep track of the product and bind the value's of the ingredients to the checkbox. If it's not checked the value must become 0 (or false).
Edit:
Parent component passing props
// React deps
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";
// Custom components
import Navigation from './components/General/Navigation'
// Pages
import Index from './components/Index'
import Category from './components/Category'
import Order from './components/Order'
// Data
import products from './Data'
class App extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
}
componentWillMount() {
setTimeout(() => {
this.setState({products});
}, 100);
}
render() {
return (
<main className="App font-sans">
<Router>
<div>
<Navigation logo="Jackies" />
<Switch>
<Route exact path="/" component={Index} />
<Route exact path="/:category" render={(props) => <Category {...props} products={this.state.products} />}/>
<Route exact path="/:category/:id" render={(props) => <Order {...props} products={this.state.products} />}/>
</Switch>
</div>
</Router>
</main>
);
}
}
export default App;
In the parent, you will pass a handler function in a onIngredientToggle prop:
<Route exact path="/:category/:id" render={(props) => <Order {...props} products={this.state.products} onIngredientToggle={this.handleIngredientToggle} />}/>
Then define the handleIngredientToggle function:
function handleIngredientToggle(productId, ingredientIndex, newIngredientValue) {
// basically this goes shallow cloning the objects and arrays up to
// the point it changes the ingredient value property
let products = [...this.state.products];
let modifiedProductIndex = products.findIndex(p => p.id === productId);
let product = {...products[modifiedProductIndex]};
products[modifiedProductIndex] = product;
product.ingredients = [...products[modifiedProductIndex].ingredients];
product.ingredients[ingredientIndex] = {...product.ingredients[ingredientIndex], value: newIngredientValue};
this.setState({products});
}
// If you prefer, the above function can be replaced with:
function handleIngredientToggle(productId, ingredientIndex, newIngredientValue) {
// deep clone products
let products = JSON.parse(JSON.stringify(this.state.products));
// modify just what changed
products.find(p => p.id === productId).ingredients[ingredientIndex].value = newIngredientValue;
this.setState({products});
}
In the Order child, you will add the index argument to the map (you have two of these, just add to the second):
{product && product.ingredients.map((item, index) => {
In the checkbox pass the product and index to the handleToggle function as argument:
<input type="checkbox" checked={item.value} name={item} onChange={(e) => this.handleToggle(e, product, index)}/>
And then in the function implementation, you will call the function received as prop from the parent:
handleToggle(e, product, index) {
this.props.onIngredientToggle(product.id, index, e.target.checked);
}
Thanks to #acdcjunior who opened my (tired) eyes, I've found a solution
Checkbox html
<input type="checkbox" checked={item.value} name={item} onChange={(e) => this.handleToggle(e, item, product)}/>
Function on the child component
handleToggle(e, item, product) {
//Get value from checkbox and set it the opposite
let value = e.target.checked ? 1 : 0;
//Pass down the item (ingredient) and parent product and also the value
this.props.toggleIngredient(item, product, value);
}
Function on parent component to change the state
toggleIngredient(i, p, v) {
// Get the product from the state
var product = this.state.products.find((product) => {
return product.id == p.id
});
// Filter down the state array to remove the product and get new products array
let products = this.state.products.filter((product) => {
return product != product;
});
// Get the ingredient object
var object = product.ingredients.find((product) => {
return product == i;
});
// Set the value for the ingredient, either true or false (depends on checkbox state)
object.value = v;
// Push the edited product the array of products
products.push(product);
// Set the state with the new products array
this.setState({
products: products
});
}

Categories