I know about props in React and how to pass props but I'm struggling to pass props to a Component that is just an object.
VideoGameList.js
const VideoGameList = [
{
id: 1,
title: "Fire Emblem Engage",
releaseDate: "01/20/2023",
price: 59.99,
quantity: 0,
},
];
My VideoGameList component is holding this array that is just an object of newly released video games. Inside each object is a quantity value that will increase from a function in another component.
VideoGame.js
const VideoGame = () => {
const [quantity, setQuantity] = useState(0);
const addToCart = () => {
setQuantity(quantity + 1);
};
return (
<>
<Header />
<div>
{VideoGameList.map(
({ title, releaseDate, price, quantity }, index) => (
<div>
<img key={src} src={src} alt={title} />
<div>
<p>{title}</p>
<p>{releaseDate}</p>
</div>
<div>
<p>${price}</p>
<button onClick={addToCart}>
Add to Cart
</button>
</div>
</div>
)
)}
</div>
</>
);
};
Inside this component you'll notice a few things. The first is I'm using the map method to display each video game in its own div. Inside the div is a picture of the game cover, the price, the release date and I have the quantity. My addToCart function increases the useState of quantity and I have a button with an onClick to increase it.
My problem is I can't seem to figure out how to use the addToCart function from my VideoGame component to increase the quantity in my VideoGameList component. The reason I'm doing this is because I have another page that will only show games that have a quantity of > 0. Please advice.
Related
i created a parent component that gathers a bunch of child components:
Expenses - Parent,
ExpenseItem - child.
i defined the first value of ExpenseItem by hard coding inside of Expenses
i entered the dynamic value to the ExpenseItem component element and then used "props" parameter to get the data from ExpenseItem to Expenses.
function Expenses() {
const Data = [{
title: `מסרק`,
date: Date(),
amount: 2.95,
},
]
return (
<div>
<ExpenseItem
title={Data[0].title}
date={Data[0].date}
amount={Data[0].amount}
/>
</div>
);
}
now, i want to update the values through a button "edit" in ExpenseItem Component.
when i do update values through useState and console log them i see the updated values,
but, the component doesnt re-renders so i see the prev value on the screen. though if i try to hard code the value it does re-renders and changes the value on the screen.
function ExpenseItem(props) {
const [title, setTitle] = useState(props.title);
const [date, setDate] = useState(props.date);
const [amount, setAmount] = useState(props.amount);
const clickHandle = () => {
console.log(title);
setTitle("חתול")
console.log(Date());
setDate(date)
console.log(amount);
setAmount("222")
}
return (
<div className="ExpenseItem">
<div className="ExpenseItem_EditFunctions">
<p className="ExpenseItemDate">{props.date}</p>
<div className="ExpenseItem_EditFunctions_Icons">
<span className="material-icons delete">delete</span>
<span className="material-icons edit" onClick={clickHandle}>
edit
</span>
</div>
<div className="ExpenseItem_MainContent">
<h3 className="ExpenseItemTitle">{props.title}</h3>
<p className="ExpenseItemAmount">₪{props.amount}</p>
</div>
</div>
</div>
);
}
It's because you are using props to display values you need to display state values instead of them.You need to replace all the prop values with corresponding state values to make this code work properly. Just replace {props.date} to {date} {props.title} to {title} and {props.amount} to {amount}
If two or more cards from a component are selected how to pass data to a button component?
I have a landing page that holds two components; Template list and a button.
<TemplateList templates={templates} />
<MenuButton style={actionButton} onClick={onOnboardingComplete}>
Select at least 2 options
</MenuButton>
The TemplateList brings in an array of information for my Template and creates Template Cards with it. I am then able to select my cards and unselect them.
My Button when pressed just takes me to the next step of the onboarding.
I would like to know how I should approach linking these two components. My button now is gray and would like this to happen:
1. Button is gray and user cant go on to next step, until selecting two or more cards.
2. When two or more cards are selected, button turn blue and they are able to press the button to continue.
This is my Template List:
export type Template = {
title: string;
description: string;
imgURL: string;
id?: number;
};
type Props = {
templates: Template[];
};
const TemplateList = ({ templates }: Props) => {
return (
<div className={styles.scrollContainer}>
{templates.map((item) => (
<TemplateCard
title={item.title}
description={item.description}
img={item.imgURL}
classNameToAdd={styles.cardContainer}
key={item.id}
/>
))}
</div>
);
};
export default TemplateList;
And this my Template Card:
type Props = {
title: string;
description: string;
img: string;
classNameToAdd?: string;
classNameOnSelected?: string;
};
const TemplateCard = ({ title, description, img, classNameToAdd, classNameOnSelected }: Props) => {
const { aspectRatio, vmin } = useWindowResponsiveValues();
let className = `${styles.card} ${classNameToAdd}`;
const [selected, setSelected] = useState(false);
const handleClick = () => {
setSelected(!selected);
};
if (selected) {
className += `${styles.card} ${classNameToAdd} ${classNameOnSelected}`;
}
return (
<div style={card} className={className} onClick={handleClick}>
<img style={imageSize} src={img}></img>
<div style={cardTitle}>
{title}
{selected ? <BlueCheckIcon style={blueCheck} className={styles.blueCheck} /> : null}
</div>
<div style={descriptionCard}>{description}</div>
</div>
);
};
TemplateCard.defaultProps = {
classNameOnSelected: styles.selected,
};
export default TemplateCard;
This is how it looks like now.
There are at least 3 ways to implement your requirement:
OPTION 1
Put Button component inside the <TemplateList> component
Add one useState tied to <TemplateList> component to hold the number of
selected cards
Add two new props onSelectCard and onDeselectCard to <TemplateCard> to increment/decrement newly created state by 1 for each selected/deselected item
Implement callback functions inside <TemplateList component (code below)
Call onSelectCard inside <TemplateCard> when needed (code below)
TemplateList Component
const TemplateList = ({ templates }: Props) => {
const [noOfSelectedCards, setNoOfSelectedCards] = useState(0);
handleSelect = () => setNoOfSelectedCards(noOfSelectedCards + 1);
handleDeselect = () => setNoOfSelectedCards(noOfSelectedCards - 1);
return (
<div classNae="template-list-container">
<div className={styles.scrollContainer}>
{templates.map((item) => (
<TemplateCard
title={item.title}
description={item.description}
img={item.imgURL}
classNameToAdd={styles.cardContainer}
key={item.id}
onSelectCard={handleSelect}
onDeselectCard={handleDeselect}
/>
))}
</div>
<MenuButton style={actionButton} onClick={onOnboardingComplete} className={noOfSelectedCards === 2 ? 'active' : ''}>
Select at least 2 options
</MenuButton>
</div>
);
};
TemplateCard Component
const TemplateCard = ({ ..., onSelectCard, onDeselectCard }: Props) => {
...
const [selected, setSelected] = useState(false);
const handleClick = () => {
if(selected) {
onDeselectCard();
} else {
onSelectCard();
}
setSelected(!selected);
};
...
};
Now, you'll have the current number of selected cards in your state noOfSelectedCards (<TemplateList> component) so you can conditionally render whatever className you want for your button or do something else with it.
OPTION 2
Using React Context to share state between components
It's okay to use React Context for such cases, but if your requirements contains other similar cases for handling/sharing states between components across the app, I suggest you take a look at the 3rd option.
OPTION 3
Using State Management like Redux to handle global/shared states between components.
This is probably the best option for projects where sharing states across the app is quite common and important. You'll need some time to understand concepts around Redux, but after you do that I assure you that you'll enjoy working with it.
I have a similar situation like the one in the sandbox.
https://codesandbox.io/s/react-typescript-fs0em
Basically what I want to achieve is that Table.tsx is my base component and App component is acting like a wrapper component. I am returning the JSX from the wrapper file.
Everything is fine but the problem is whenever I hover over any name, getData() is called and that is too much rerendering. Here it is a simple example but in my case, in real, the records are more.
Basically Table is a generic component which can be used by any other component and the data to be displayed in can vary. For e.g. rn App is returning name and image. Some other component can use the Table.tsx component to display name, email, and address. Think of App component as a wrapper.
How can I avoid this getData() to not to be called again and again on hover?
Can I use useMemo or what approach should I use to avoid this?
Please help
Every time you update the "hover" index state in Table.jsx it rerenders, i.e. the entire table it mapped again. This also is regenerating the table row JSX each time, thus why you see the log "getData called!" so much.
You've effectively created your own list renderer, and getData is your "renderRow" function. IMO Table shouldn't have any state and the component being rendered via getData should handle its own hover state.
Create some "row component", i.e. the thing you want to render per element in the data array, that handles it's own hover state, so when updating state it only rerenders itself.
const RowComponent = ({ index, name }) => {
const [hov, setHov] = useState();
return (
<div
key={name}
onMouseEnter={() => setHov(index)}
onMouseLeave={() => setHov(undefined)}
style={{ display: "flex", justifyContent: "space-around" }}
>
<div> {name} </div>
<div>
<img
src={hov === index ? img2 : img1}
height="30px"
width="30px"
alt=""
/>
</div>
</div>
);
};
Table.jsx should now only take a data prop and a callback function to render a specific element, getData.
interface Props {
data: string[];
getData: () => JSX.Element;
}
export const Table: React.FC<Props> = ({ data, getData }) => {
return (
<div>
{data.map((name: string, index: number) => getData(name, index))}
</div>
);
};
App
function App() {
const data = ["Pete", "Peter", "John", "Micheal", "Moss", "Abi"];
const getData = (name: string, index: number, hov: number) => {
console.log("getData called!", index);
return <RowComponent name={name} index={index} />;
};
return <Table data={data} getData={getData} />;
}
I have two functional components built in React, one is an Item component - it holds some data about stuff, with optional graphics, some text data and price information. On the bottom there is a button, that allows you to select this particular item. It also keeps information in its props on ID of currently selected Item - that's how i planned to solve this problem.
My second component is a ItemList - it basically holds a list of aforemetioned Items - plus it sorts all the items and must keep information about which component is currently selected - the selected one basically looks different - some stuff like the border box and button's colour gets switched via CSS.
My logic to implement goes like this - when user clicks on a "Select" button of a particular Item, the Item should change its look (unless it's already selected, then do nothing), after that somehow propagate info up onto the ItemList, so that it can "disable" the previously selected component. There can be only one selected Item, and once user decide to select another one, the previously selected should change its state and go back to unselected standard graphic style.
I've ran across a solution with state in the ItemList component plus passing a function via props into Item, but that doesn't solve the second part - ItemList needs to get info about a change, so it can rerender all the components according to actual state. What part of React API should I dive into to solve this issue?
Here is code for my components:
Item
interface Props {
receivedObject: itemToDisplay;
selectedItemId: string;
onClick?: () => void;
}
export default function Item(props: Props) {
const {name, description, price} = props.receivedObject;
const imageUrl = props.receivedObject?.media?.mainImage?.small?.url;
const priceComponent = <Price price={price}/>;
const [isItemSelected, setSelection] = useState(props.selectedItemId == props.receivedObject.id);
const onClick = props.onClick || (() => {
setSelection(!isItemSelected)
});
return (
<>
<div className="theDataHolderContainer">
// displayed stuff goes here
<div className="pickButtonContainer">
// that's the button which should somehow send info "upwards" about the new selected item
<Button outline={isItemSelected} color="danger" onClick={onClick}>{isItemSelected ? "SELECTED" : "SELECT"}</Button>
</div>
</div>
</>)
};
ItemList
interface Props {
packageItems: Array<itemToDisplay>
}
export default function ItemList(props: Props) {
const itemsToDisplay = props.packageItems;
itemsToDisplay.sort((a, b) =>
a.price.finalPrice - b.price.finalPrice
);
let selectedItemId = itemsToDisplay[0].id;
const [currentlySelectedItem, changeCurrentlySelectedItem] = useState(selectedItemId);
const setSelectedItemFunc = () => {
/* this function should be passed down as a prop, however it can only
* have one `this` reference, meaning that `this` will refer to singular `Item`
* how do I make it change state in the `ItemList` component?
*/
console.log('function defined in list');
};
return(
<div className="packageNameList">
<Item
key={itemsToDisplay[0].id}
receivedObject={itemsToDisplay[0]}
onClick={setSelectedItemFunc}
/>
{itemsToDisplay.slice(1).map((item) => (
<Item
key={item.id}
receivedObject={item}
onClick={setSelectedItemFunc}
/>
))}
</div>
);
}
In React, the data flows down, so you'd better hold state data in a stateful component that renders presentation components.
function ListItem({ description, price, selected, select }) {
return (
<li className={"ListItem" + (selected ? " selected" : "")}>
<span>{description}</span>
<span>${price}</span>
<button onClick={select}>{selected ? "Selected" : "Select"}</button>
</li>
);
}
function List({ children }) {
return <ul className="List">{children}</ul>;
}
function Content({ items }) {
const [selectedId, setSelectedId] = React.useState("");
const createClickHandler = React.useCallback(
id => () => setSelectedId(id),
[]
);
return (
<List>
{items
.sort(({ price: a }, { price: b }) => a - b)
.map(item => (
<ListItem
key={item.id}
{...item}
selected={item.id === selectedId}
select={createClickHandler(item.id)}
/>
))}
</List>
);
}
function App() {
const items = [
{ id: 1, description: "#1 Description", price: 17 },
{ id: 2, description: "#2 Description", price: 13 },
{ id: 3, description: "#3 Description", price: 19 }
];
return (
<div className="App">
<Content items={items} />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
.App {
font-family: sans-serif;
}
.List > .ListItem {
margin: 5px;
}
.ListItem {
padding: 10px;
}
.ListItem > * {
margin: 0 5px;
}
.ListItem:hover {
background-color: lightgray;
}
.ListItem.selected {
background-color: darkgray;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
I am adding a component onclick and keeping track of the components using useState Array. However when I go to remove one of the added components, it doesn't recognize the full component Array size, only the state that was there when that component was initially added.
Is there a way to have the current state recognized within that delete function?
https://codesandbox.io/s/twilight-water-jxnup
import React, { useState } from "react";
export default function App() {
const Span = props => {
return (
<div>
<span>{props.index}</span>
<button onClick={() => deleteSpan(props.index)}>DELETE</button>
Length: {spans.length}
</div>
);
};
//set initial span w/ useState
const [spans, setSpans] = useState([<Span key={0} index={Math.random()} />]);
//add new span
const addSpan = () => {
let key = Math.random();
setSpans([...spans, <Span key={key} index={key} />]);
};
//delete span
const deleteSpan = index => {
console.log(spans);
console.log(spans.length);
};
//clear all spans
const clearInputs = () => {
setSpans([]);
};
return (
<>
{spans}
<button onClick={() => addSpan()}>add</button>
<button onClick={() => clearInputs()}>clear</button>
</>
);
}
UPDATE - Explaining why you are facing the issue descibed on your question
When you are adding your new span on your state, it's like it captures an image of the current values around it, including the value of spans. That is why logging spans on click returns you a different value. It's the value spans had when you added your <Span /> into your state.
This is one of the benefits of Closures. Every <Span /> you added, created a different closure, referencing a different version of the spans variable.
Is there a reason why you are pushing a Component into your state? I would suggest you to keep your state plain and clean. In that way, it's also reusable.
You can, for instance, use useState to create an empty array, where you will push data related to your spans. For the sake of the example, I will just push a timestamp, but for you might be something else.
export default function App() {
const Span = props => {
return (
<div>
<span>{props.index}</span>
<button onClick={() => setSpans(spans.filter(span => span !== props.span))}>DELETE</button>
Length: {spans.length}
</div>
);
};
const [spans, setSpans] = React.useState([]);
return (
<>
{spans.length
? spans.map((span, index) => (
<Span key={span} index={index} span={span} />
))
: null}
<button onClick={() => setSpans([
...spans,
new Date().getTime(),
])}>add</button>
<button onClick={() => setSpans([])}>clear</button>
</>
);
}
I hope this helps you find your way.