How to get which element I have clicked on? | React - javascript

I render 12 cards on the screen each of which have a unique key attribute.
I only started learning react and I want function to only to be called when a card has been clicked and save only that key which been clicked in state if an other has been clicked then update the state and then have 2 ids there and not all 12.
This is my Card component (the others are irrelevant I think)
import React, {useState} from "react";
import {CardData} from "./Data/CardsData.js"
import style from "../style/Cards.css"
import Score from "./Score.js";
const Cards = () =>{
const [bestScore, setBestScore] = useState(0);
const [scoreRN, setScoreRN] = useState(0);
const [clickedCard, setClickedCard] =useState([]);
const CardClick = () =>{
for(let card of CardData)
setClickedCard( clickedCard.concat(card))
console.log(clickedCard)
}
return(
<div id="content">
<Score bestScore={bestScore} scoreRN={scoreRN}/>
{CardData.map((data) => {
return(
<div key={data.id} className="card" onClick={CardClick}>
<img src={data.img}></img>
<h2>{data.name}</h2>
</div>
)
})}
</div>
)
}
export default Cards;
This way I only get the last card's id into my clickedCard array, every time I click on a card.
I have also could do it where I added every cardid on every click.
And an additional thing that I don't fully understand: Why is it that now the 1. console log is returns an empty array (then 1, 2,3 ... elemement)?

CardClick callback is a MouseEventHandler so it receives event as first argument.
You can then get event.currentTarget.id in the CardClick function (if your element has an id)
Another way is to explicit pass data to the onClick handler like onClick={event => CardClick(event, data.id)}. Then your CardClick function can accept your aditionnal arguments

You don't need the for loop if you just want to add the clicked card into the array, but you do need to pass a card parameter so that the callback knows which card was clicked:
const CardClick = (card) =>{
// for(let card of CardData) - you can remove this line
setClickedCard( clickedCard.concat(card))
console.log(clickedCard)
}
In your JSX:
<div key={data.id} className="card" onClick={() => CardClick(data)}>
<img src={data.img}></img>
<h2>{data.name}</h2>
</div>
Also, your console.log is returning an empty array because react state changes aren't immediate, so even after you called setClickedCard, clickedCard will still be its old value in the same function until React's next render.

Related

React use effect causing infinite loop

Am new to react and am creating a custom select component where its supposed to set an array selected state and also trigger an onchange event and pass it to the parent with the selected items and also get initial value as prop and set some data.
let firstTime = true;
const CustomSelect = (props)=>{
const [selected, setSelected] = useState([]);
const onSelectedHandler = (event)=>{
// remove if already included in the selected items remove
//otherwise add
setSelected((prev)=>{
if (selected.includes(value)) {
values = prevState.filter(item => item !== event.target.value);
}else {
values = [...prevState, event.target.value];
}
return values;
})
// tried calling props.onSelection(selected) but its not latest value
}
//watch when the value of selected is updated and pass onchange to parent
//with the newest value
useEffect(()=>{
if(!firstTime && props.onSelection){
props.onSelection(selected);
}
firstTime = false;
},[selected])
return (<select onChange={onSelectedHandler}>
<option value="1"></option>
</select>);
};
Am using it on a parent like
const ParentComponent = ()=>{
const onSelectionHandler = (val)=>{
//do stuff with the value passed
}
return (
<CustomSelect initialValue={[1,2]} onSelection={onSelectionHandler} />
);
}
export default ParentComponent
The above works well but now the issue comes in when i want to set the initialValue passed from the parent onto the customSelect by updating the selected state. I have added the followin on the CustomSelect, but it causes an infinite loop
const {initialValue} = props
useEffect(()=>{
//check if the value of initialValue is an array
//and other checks
setSelected(initialValue)
},[initialValue]);
I understand that i could have passed the initialValue in the useState but i would like to do a couple of checks before setting the selected state.
How can i resolve this, am still new to react.
In your //do stuff with the value passed you are most likely update the states of your component parent component and it causes to rerender parent component. When passing the prop initialValue={[1,2]} creates a new instance of [1,2] array on each render and causes the infinite render on useEffect. In order to solve this, you can move the initialValue prop to somewhere else as const value like this:
const INITIAL_VALUE_PROP = [1,2];
const ParentComponent = ()=>{
const onSelectionHandler = (val)=>{
//do stuff with the value passed
}
return (
<CustomSelect initialValue={INITIAL_VALUE_PROP} onSelection={onSelectionHandler}
/>
);
}
export default ParentComponent
Okay, so the reason this is happening is that you're passing the initialValue prop as an array, and since this is a reference value in JavaScript, it means that each time it's passed(updated), it's passed with a different reference value/address, and so the effect will continue to re-run infinitely. One way to solve this is to use React.useMemo, documentation here to store/preserve the reference value of the array passed, not to cause unnecessary side effects running.

Why won't the updated variable values display?

I am setting initial values on a these variables. I change the values, and they do log correctly, however in the return function, they only display the original values. How do I make sure they show the correct updated values?
I declare my variables:
let teamBillableData = [];
let teamMemberBillableAmount = 0;
let teamMemberIndex = 0;
Then I change the values:
teamBillableData = parsedData.results;
teamMemberIndex = teamBillableData.findIndex(function (teamMember) {
return teamMember.user_name === teamMemberName;
})
teamMemberBillableAmount = teamBillableData[teamMemberIndex].billable_amount;
When I log the variables, they are correct:
console.log(teamMemberIndex); <---- Returns correct new value of 1
console.log(teamMemberBillableAmount); <---- Returns correct new value of 1,221.25
However, when I render the values in my return function in my React app, they render the initial values stull:
return (
<div>
<div>
<div>
<h5>{teamMemberIndex}</h5> <---- Returns old original value of 0
<h5>${teamMemberBillableAmount.toLocaleString()} </h5> <---- Returns old original value of 0
</div>
</div>
</div>
);
};
I assume it's rendering before the values are changed. But I do not know how to make it render AFTER they values are changed.
A React component only re-renders after either its state or its props change.
If you want your component to re-render when a variable changes, you have to add said variable to the component's state
If you're dealing with functional components, look into useState hook.
As told by Michael and Shubham you can write a functional component that triggers a rerender each time the state is updated using the useState hook.
An Example could be:
import React, { useState } from "react";
function Example() {
const [teamMemberBillableAmount, setTeamMemberBillableAmount] = useState(0);// initial state
const clickHandler = () => setTeamMemberBillableAmount(teamMemberBillableAmount + 1); // add 1 to the teamMemberBillableAmount state each time the button is clicked (a rerender will happen)
return (
<div>
<p>The team billable amount is:<strong style={{ color: "red" }}> teamMemberBillableAmount}</strong></p>
<button onClick={clickHandler}>Press me to add +1</button>
</div>
);
}
You can run and play with this code here

Unexpected behavior with useState hook React

I have some experience with class components in React, but am trying to learn hooks and functional components better.
I have the following code:
import React, { useState } from "react";
import { Button } from "reactstrap";
import StyleRow from "./StyleRow";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = styles;
newStyles.splice(index, 1);
setStyles(newStyles);
}
return (
<div>
{styles}
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
</div>
);
}
The goal of this code is to have an array of components that have an "x" button next to each one that removes that specific component, as well as a "+" button at the bottom that adds a new one. The StyleRow component just returns a paragraph JSX element right now.
The unusual behavior is that when I click the "x" button by a row, it removes the element and all elements following it. It seems that each new StyleRow component that is added takes the state of styles at the moment of its creation and modifies that instead of always modifying the current styles state. This is different behavior than I would expect from a class component.
The freezing of state leads me to believe this has something to do with closures, which I don't fully understand, and I am curious to know what here triggered them. If anyone knows how to solve this problem and always modify the same state, I would greatly appreciate it.
Finally, I think this post on SO is similar, but I believe it addresses a slightly different question. If someone can explain how that answer solves this problem, of course feel free to close this question. Thank you in advance!
You are modifying the existing state of styles, so you will need to create a deep copy of the array first.
You can either write your own clone function, or you can import the Lodash cloneDeep function.
Add the following dependency to your package.json using:
npm install lodash
Also, you are passing the length of the array to the removeStyle function. You should be passing the last index which is length - 1.
// ...
import { cloneDeep } from 'lodash';
// ...
function removeStyle(index) {
let newStyles = cloneDeep(styles); // Copy styles
newStyles.splice(index, 1); // Splice from copy
setStyles(newStyles); // Assign copy to styles
}
// ...
<Button onClick={() => removeStyle(styles.length - 1)}>x</Button>
// ...
If you want to use a different clone function or write your own, there is a performance benchmark here:
"What is the most efficient way to deep clone an object in JavaScript?"
I would also move the function assigned to the onClick event handler in the button outside of the render function. It looks like you are calling setStyles which adds a button with a removeStyle event which itself calls setStyles. Once you move it out, you may be able to better diagnose your issue.
Update
I rewrote your component below. Try to render elements using the map method.
import React, { useState } from "react";
import { Button } from "reactstrap";
const Controls = (props) => {
const [styles, setStyles] = useState([]);
const removeStyle = (index) => {
const newStyles = [...styles];
newStyles.splice(index, 1);
setStyles(newStyles);
};
const getChildNodeIndex = (elem) => {
let position = 0;
let curr = elem.previousSibling;
while (curr != null) {
if (curr.nodeType !== Node.TEXT_NODE) {
position++;
}
curr = curr.previousSibling;
}
return position;
};
const handleRemove = (e) => {
//removeStyle(parseInt(e.target.dataset.index, 10));
removeStyle(getChildNodeIndex(e.target.closest("div")));
};
const handleAdd = (e) => setStyles([...styles, styles.length]);
return (
<div>
{styles.map((style, index) => (
<div key={index}>
{style}
<Button data-index={index} onClick={handleRemove}>
×
</Button>
</div>
))}
<Button color="primary" onClick={handleAdd}>
+
</Button>
</div>
);
};
export default Controls;
I've added the most preferred way in a new answer as the previous one was becoming too long.
The explanation lies in my previous answer.
import React, { useState } from "react";
import { Button } from "reactstrap";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = [...styles]
newStyles.splice(index, 1);
setStyles(newStyles);
}
const addStyle = () => {
const newStyles = [...styles];
newStyles.push({content: 'ABC'});
setStyles(newStyles);
};
// we are mapping through styles and adding removeStyle newly and rerendering all the divs again every time the state updates with new styles.
// this always ensures that the removeStyle callback has reference to the latest state at all times.
return (
<div>
{styles.map((style, index) => {
return (
<div>
<p>{style.content} - Index: {index}</p>
<Button onClick={() => removeStyle(index)}>x</Button>
</div>
);
})}
<Button color="primary" onClick={addStyle}>+</Button>
</div>
);
}
Here is a CodeSandbox for you to play around.
Let's try to understand what's going on here.
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
First render:
// styles = []
You add a new style.
// styles = [<div1>]
The remove callback from the div is holding the reference to styles, whose length is now 0
You add one more style. // styles = [<div1>, <div2>]
Since div1 was created previously and didn't get created now, its still holding a reference to styles whose length is still 0.
div2 is now holding a reference to styles whose length is 1.
Now the same goes for the removeStyle callback that you have. Its a closure, which means it's holding a reference to a value of its outer function, even after the outer function has done executing. So when removeStyles is called for the first div1 the following lines will execute:
let newStyles = styles; // newStyles []
newStyles.splice(index, 1); // index(length) = 0;
// newStyles remain unchanged
setStyles(newStyles); // styles = [] (new state)
Now consider you have added 5 styles. So this is how the references will be held by each div
div1 // styles = [];
div2 // styles = [div1];
div3 // styles = [div1, div2];
div4 // styles = [div1, div2, div3];
div5 // styles = [div1, div2, div3, div4];
So what happens if you try to remove div3, the following removeStyly will execute:
let newStyles = styles; // newStyles = [div1, div2]
newStyles.splice(index, 1); // index(length) = 2;
// newStyles remain unchanged; newStyles = [div1, div2]
setStyles(newStyles); // styles = [div2, div2] (new state)
Hope that helps and addresses your concern. Feel free to drop any questions in the comments.
Here is a CodeSandbox for you to play around with and understand the issue properly.

Strange behaviour when removing element from array of divs (React Hooks)

I am struggling to understand a strange behaviour while deleting an element from an array of divs.
What I want to do is create an array of divs representing a list of purchases. Each purchase has a delete button that must delete only the clicked one. What is happening is that when the delete button is clicked on the purchase x all the elements with indexes greather than x are deleted.
Any help will be appreciated, including syntax advices :)
import React, { useState } from "react";
const InvestmentSimulator = () => {
const [counter, increment] = useState(0);
const [purchases, setPurchases] = useState([
<div key={`purchase${counter}`}>Item 0</div>
]);
function addNewPurchase() {
increment(counter + 1);
const uniqueId = `purchase${counter}`;
const newPurchases = [
...purchases,
<div key={uniqueId}>
<button onClick={() => removePurchase(uniqueId)}>delete</button>
Item number {uniqueId}
</div>
];
setPurchases(newPurchases);
}
const removePurchase = id => {
setPurchases(
purchases.filter(function(purchase) {
return purchase.key !== `purchase${id}`;
})
);
};
const purchasesList = (
<div>
{purchases.map(purchase => {
if (purchases.indexOf(purchase) === purchases.length - 1) {
return (
<div key={purchases.indexOf(purchase)}>
{purchase}
<button onClick={() => addNewPurchase()}>add</button>
</div>
);
}
return purchase;
})}
</div>
);
return <div>{purchasesList}</div>;
};
export default InvestmentSimulator;
There are several issues with your code, so I'll go through them one at a time:
Don't store JSX in state
State is for storing serializable data, not UI. You can store numbers, booleans, strings, arrays, objects, etc... but don't store components.
Keep your JSX simple
The JSX you are returning is a bit convoluted. You are mapping through purchases, but then also returning an add button if it is the last purchase. The add button is not related to mapping the purchases, so define it separately:
return (
<div>
// Map purchases
{purchases.map(purchase => (
// The JSX for purchases is defined here, not in state
<div key={purchase.id}>
<button onClick={() => removePurchase(purchase.id)}>
delete
</button>
Item number {purchase.id}
</div>
))}
// An add button at the end of the list of purchases
<button onClick={() => addNewPurchase()}>add</button>
</div>
)
Since we should not be storing JSX in state, the return statement is where we turn our state values into JSX.
Don't give confusing names to setter functions.
You have created a state variable counter, and named the setter function increment. This is misleading - the function increment does not increment the counter, it sets the counter. If I call increment(0), the count is not incremented, it is set to 0.
Be consistent with naming setter functions. It is the accepted best practice in the React community that the setter function has the same name as the variable it sets, prefixed with the word "set". In other words, your state value is counter, so your setter function should be called setCounter. That is accurate and descriptive of what the function does:
const [counter, setCounter] = useState(0)
State is updated asynchronously - don't treat it synchronously
In the addNewPurchase function, you have:
increment(counter + 1)
const uniqueId = `purchase${counter}`
This will not work the way you expect it to. For example:
const [myVal, setMyVal] = useState(0)
const updateMyVal = () => {
console.log(myVal)
setMyVal(1)
console.log(myVal)
}
Consider the above example. The first console.log(myVal) would log 0 to the console. What do you expect the second console.log(myVal) to log? You might expect 1, but it actually logs 0 also.
State does not update until the function finishes execution and the component re-renders, so the value of myVal will never change part way through a function. It remains the same for the whole function.
In your case, you're creating an ID with the old value of counter.
The component
Here is an updated version of your component:
const InvestmentSimulator = () => {
// Use sensible setter function naming
const [counter, setCounter] = useState(0)
// Don't store JSX in state
const [purchases, setPurchases] = useState([])
const addNewPurchase = () => {
setCounter(prev => prev + 1)
setPurchases(prev => [...prev, { id: `purchase${counter + 1}` }])
}
const removePurchase = id => {
setPurchases(prev => prev.filter(p => p.id !== id))
}
// Keep your JSX simple
return (
<div>
{purchases.map(purchase => (
<div key={purchase.id}>
<button onClick={() => removePurchase(purchase.id)}>
delete
</button>
Item number {purchase.id}
</div>
))}
<button onClick={() => addNewPurchase()}>add</button>
</div>
)
}
Final thoughts
Even with these changes, the component still requires a bit of a re-design.
For example, it is not good practice to use a counter to create unique IDs. If the counter is reset, items will share the same ID. I expect that each of these items will eventually store more data than just an ID, so give them each a unique ID that is related to the item, not related to its place in a list.
Never use indices of an array as key. Check out this article for more information about that. If you want to use index doing something as I did below.
const purchasesList = (
<div>
{purchases.map((purchase, i) => {
const idx = i;
if (purchases.indexOf(purchase) === purchases.length - 1) {
return (
<div key={idx}>
{purchase}
<button onClick={() => addNewPurchase()}>add</button>
</div>
);
}
return purchase;
})}
</div>
);

Passing a function through three levels of components (i.e. calling a function from a grandchild component)

I want to pass an item id from either a Child component or grandchild component, but cannot figure out how to do so. Other examples I have looked at show using the arrow function to achieve this, but for whatever reason my function is not getting called.
I have the following in Parent.js:
chosenItem(id){
console.log("CHOSEN ITEM SELECTED")
this.setState({
chosen_item: id
})
}
and in the Parent.js render function:
<Child objects={objects} chosenItem={() => this.chosenItem()} />
Then in my Child.js I have:
items = this.props.objects.items.map(item => {
return (
<ClickableTextComponent
key={item.id}
text={item.label}
onClick={item =>this.props.chosenItem(item.id)}
/>
)
})
In my Child.js render function I have:
{items}
I also wasn't sure whether the actual click event should go inside of the Child.js or the ClickableTextComponent. Or does it really matter? Currently I have placed it in the Child.js component as seen above.
What am I doing wrong? How can I modify my code so that the function gets called? I have read a bit about currying to prevent a function from being recreated multiple times. Is that necessary in this case? If so, where and how should I be implementing it. In my Parent or Child components?
Update
I was previously trying to get the onClick to work from Child.js, but as it needs to be attached to a div I have moved it to ClickableTextComponent (the grandchild component).
One issue with ClickableTextComponent is that I want to be able to set the state when the component is clicked so that I can turn the component a different colour. Because of that I am needing to use a function to then call the chosenItem function. So here is what I have in my `ClickableTextComponent.js':
handleClick(){
this.setState({
text_state: "clicked"
})
this.props.chosenItem()
}
In the render I then have:
<div
onClick={this.handleClick.bind(this)}
onMouseOver={this.changeTextState.bind(this, "hover")}
onMouseOut={this.changeTextState.bind(this, "default")}
>{this.props.text}</div>
New Error
Based on the above changes, I am now getting this.props.chosenItem is not a function. However, I cannot figure out why it is giving me this error. I can see the function name when I display this.props to the console. What am I doing wrong?
The answer given by Kevin He holds true. But there is one problem with that solution.
<Child objects={objects} chosenItem={(x) => this.chosenItem(x)} />
When you do such, every time your parent is rerendered. It will create a new instance of the function. And, your child component also rerenders because It sees the props changing.
Best solution is:
<Child objects={objects} chosenItem={this.chosenItem} />
Update:
Now, it seems to make sense.
The problem is again with ClickableTextComponent.
Here is the update ClickableTextComponent which works.
https://codesandbox.io/s/73x6mnr8k0
The main problem:
items = this.props.objects.items.map(item => {
return (
<ClickableTextComponent
key={item.id}
text={item.label}
onClick={item =>this.props.chosenItem(item.id)}
/>
)
})
//
// Here you made a function (item) => this.props.choseItem(item.id)
// That means when you call that function you should call like this
// i.e. passing parameter needed for the function
//
handleClick(){
this.setState({
text_state: "clicked"
})
this.props.chosenItem(item)
}
//
// But do you do not have the item in the children
// Parent should be changed as below
//
items = this.props.objects.items.map(item => {
return (
<ClickableTextComponent
key={item.id}
text={item.label}
onClick={() =>this.props.chosenItem(item.id)}
/>
)
})
//
// Now you made a fuction () => this.props.chosenItem(item.id)
// The main difference being you are not taking a item as parameter
// item will be taken from outer scope that means, item from map
//
//
// Another solution can be
//
items = this.props.objects.items.map(item => {
return (
<ClickableTextComponent
key={item.id}
id={item.id}
text={item.label}
onClick={this.props.chosenItem}
/>
)
})
// And in ClickableTextComponent
handleClick(){
this.setState({
text_state: "clicked"
})
this.props.chosenItem(this.props.id)
}
You can do this:
<Child objects={objects} chosenItem={(x) => this.chosenItem(x)} />
Note that chosenItem is a function, then whenever it's called with item.id, it will take call the function this.chosenItem at the parent element.

Categories

Resources