onClick method runs setState multiple times in a row, negating intended purpose - reactjs

I'm attempting to have my onClick method handleClick set the state of the active and groupCardInfo properties. active in particular is a boolean, and I'm using this bool value to determine whether a side menu item should be expanded or not.
SideMenuContainer component that calls handleClick:
render() {
if (this.props.active == true){
return (
<ParentContainer>
<p onClick={this.props.handleClick(this.props.properties)}>{this.props.parentName}</p>
<NestedContainer>
{this.props.properties.map(propertyElement => {
return (
<NestedProperty onClick={() => { this.props.changeInfoList(propertyElement.name, propertyElement.data_type, propertyElement.app_keys)}} >
{propertyElement.name}
</NestedProperty>
);
})}
</NestedContainer>
</ParentContainer>
);
}
The issue is that clicking that <p> results in handleClick running multiple times. so rather than toggling the active value from false to true, it toggles it back and forth multiple times so that it goes from false back to false again.
What's incorrect with the way I'm structuring this method in the parent App.js that's causing this?:
handleClick(properties){
console.log("toggle click!")
// this.setState({active : !this.state.active});
this.setState({
active: !this.state.active,
groupedCardInfo: properties
})
console.log("the active state is now set to: " + this.state.active)
}

It's because you are invoking the function in event handler. The first time render runs it will execute your event handler. You can do this like your other onClick handler:
<p onClick={() => { this.props.handleClick(this.props.properties) }}>{this.props.parentName}</p>
Or you can do it like this:
<p onClick={this.props.handleClick}>{this.props.parentName}</p>
But then you would have to change how you reference properties in your click handler. Like this:
handleClick(){
const properties = this.props.properties
this.setState({
active: !this.state.active,
groupedCardInfo: properties
})
console.log("the active state is now set to: " + this.state.active)
}

Try using arrow function, like you did on the other onClick:
<p onClick={() => this.props.handleClick(this.props.properties)}>
Calling this.props.handleClick... while rendering, invokes it.
Then, setting the state, makes the component to re render.

Related

How do you cleanly convert React setState callback to useEffect hooks?

I can create a class component with 2 buttons, "A" and "B". Button A should set a state variable and then take some action. Button B should also set the same state variable and then take a different action. They key thing that this hinges on is the second argument to setState, which is the callback function for what action to take after the state has been set.
When I try to write this as a function component, I run into an issue. I move the callbacks into a hook (or multiple hooks), but I have no way to know which button was clicked. Furthermore, the hook does not trigger every time I click the button. The hook only triggers when the button results in a real state change (e.g. changing 5 to 5 skips the effect).
Here is a simple example:
import React from 'react';
import './styles.css';
// actionA and actionB represent things that I want to do when the
// buttons are clicked but after the corresponding state is set
const actionA = () => {
console.log('button A thing');
};
const actionB = () => {
console.log('button B thing');
};
// render 2 components that should behave the same way
const App = () => {
return (
<>
<ClassVersion />
<FunctionVersion />
</>
);
};
// the class version uses this.state
class ClassVersion extends React.Component {
constructor(props) {
super(props);
this.state = {
x: 0,
};
}
render = () => {
return (
<div className='box'>
Class Version
<br />
<button
type='button'
onClick={() => {
this.setState({ x: 1 }, () => {
// here I can do something SPECIFIC TO BUTTON A
// and it will happen AFTER THE STATE CHANGE
actionA();
});
// can't call actionA here because the
// state change has not yet occurred
}}
>
A
</button>
<button
type='button'
onClick={() => {
this.setState({ x: 2 }, () => {
// here I can do something SPECIFIC TO BUTTON B
// and it will happen AFTER THE STATE CHANGE
actionB();
});
// can't call actionB here because the
// state change has not yet occurred
}}
>
B
</button>
State: {this.state.x}
</div>
);
};
}
// the function version uses the useState hook
const FunctionVersion = () => {
const [x, setX] = React.useState(0);
React.useEffect(() => {
// the equivalent "set state callback" does not allow me to
// differentiate WHY X CHANGED. Was it because button A was
// clicked or was it because button B was clicked?
if (/* the effect was triggered by button A */ true) {
actionA();
}
if (/* the effect was triggered by button B */ true) {
actionB();
}
// furthermore, the effect is only called when the state CHANGES,
// so if the button click does not result in a state change, then
// the EFFECT (AND THUS THE CALLBACK) IS SKIPPED
}, [x]);
return (
<div className='box'>
Function Version
<br />
<button
type='button'
onClick={() => {
// change the state and trigger the effect
setX(1);
// can't call actionA here because the
// state change has not yet occurred
}}
>
A
</button>
<button
type='button'
onClick={() => {
// change the state and trigger the effect
setX(2);
// can't call actionB here because the
// state change has not yet occurred
}}
>
B
</button>
State: {x}
</div>
);
};
export default App;
I came up with a couple ideas:
Change the type of x from number to {value: number; who: string}. Change calls from setX(1) to setX({value: 1, who: 'A'}). This will allow the effect to know who triggered it. It will also allow the effect to run every time the button is clicked because x is actually getting set to a new object each time.
Change the type of x from number to {value: number; callback: () => void}. Change calls from setX(1) to setX({value: 1, callback: actionA}). Same as above, but slightly different. The effect would say if(x.callback) { x.callback() }.
Both these ideas seem to work, but the problem is I don't want to keep adding an extra tag to my state variables just to keep track of which thing triggered them.
Is my idea the correct solution, or is there a better way to do this that doesn't involve "tagging" my state variables?
How about this:
const [a, setA] = React.useState(0);
const [b, setB] = React.useState(0);
React.useEffect(() => {
actionA();
}, [a]);
React.useEffect(() => {
actionB();
}, [b]);
onClick={() => {
setX(1);
setA(a => a + 1);
}}
onClick={() => {
setX(2);
setB(b => b + 1);
}}
Let's say you put actionA and actionB in the corresponding button A/B onClick(). Given that you WILL be updating state based on the action of this onClick, you could directly call the action functions during the onClick if you pass the new state data to the functions.
onClick={() => {
actionA(newStateData);
setX(newStateData);
}}
If you don't want to do this, you could use:
your solution of adding an identifier to the state value would be fine!
Create separate state values for A and B

Customize Array State in ReactJs with ES6

i am fetching some data (in array) from api and assigning it to state called items. then calling an Item component for each item of the state by following code:
<div>
{items.length} results found for {search_term}
{items.map((item) =>
(item.active_price > 0 && item.show) ? <Item key={item.id} details={item} items={items} setItems={setItems}/> : ""
)}
</div>
The array looks like this:
Next plan is adding a remove button in Item component, which will change value of show to false (for the item clicked) in the items state. What I am thinking is, as the state will change, the items state will be re-rendered and the item turned to false will be hidden from view. I am adding a onCLick event listener on the button and the function is:
const handleClick = () => {
console.log(({...props.items,[props.details.number - 1]:false}))
// props.setItems(prevState=>({...prevState,[props.details.number - 1]:false}))
}
I'll call props.setItems later but for now, I am trying to see if I can just edit that element to turn the show into false. the code above replace the whole index with false.
in the onClick function, I only want to edit the value of show, not replace the entire entry. I have tried:
({...props.items,[props.details.number - 1][show]:false})
and
({...props.items,[props.details.number - 1].show:false})
It shows syntax error before [show] and .show respectively. How can I edit my handleClick function to edit the value of show properly.
Thanks.
It looks like you are converting your array into an object in your current method. (the first screen shot is an array, the second is an object). You're going to run into issues there.
To just remove the item from the array, it would be easiest just to use .filter();
const handleClick = () => {
props.setItems(
prevState => prevState.filter(item => item.number !== props.details.number)
);
}
To set the show property is a bit more complicated. This is pretty standard way to do so without mutating.
const handleClick = () => {
props.setItems(prevState => {
const itemIndex = prevState.findIndex(
item => item.number == props.details.number
);
// Using 'props.details.number - 1' will not be a valid index when removing items
return [
...prevState.slice(0, itemIndex), // Your state before the item being edited
{...prevState[itemIndex], show: false}, // The edited item
...prevState.slice(itemIndex + 1) // The rest of the state after the item
];
});
}
You can try the following syntax of code, assuming items is state here-
const [items,setItems]=.....
The following is the code
const handleClick = () => {
setItems({
...items,
show: false,
})}
It will update the items state with value show as false.

How to Toggle Multiple Hide/Display state

I am having trouble coming up with a method that allows me to toggle multiple displays on and off without hard-coding a function for each displayed component.
I was wondering if there is a way to toggle these display states with one function kind of like and onChange event.
this.state = {
showOne: false,
showTwo: false
}
display = () => {
let { name, value } = e.target
this.setState({ [name]: !value })
}
return(
<button name={showOne} value={this.state.showOne} onClick={this.display}>
{!showOne
? (
null
) : (
<div><ComponentOne/></div>
)}
</button
<button name={showtwo} value={this.state.showTwo} onClick={this.display}>
</button
{!showTwo
? (
null
) : (
<div><ComponentTwo/></div>
)}
this only somewhat works the issue is that it changes the state to a string instead of a Boolean. aka showOne: false => showOne: 'false'.
I know the name and value properties are for inputs but I was wondering if there was something similar along these lines so I can have one function that allows the displaying/un-displaying of multiple components.
You can set them up with an inline function that passes in an identifier, which is then used as a key in the state:
onClick={() => toggle(itemName)}
toggle = (itemName) => {
this.setState({ [itemName]: !this.state[itemName] });
}
Another option is to define a component for the button that takes name and onClick props, and have the component’s internal onClick invoke the prop onClick with the name. (It’s hard to write example code from the phone. I can update this if it isn’t clear.)
toggleDisplay = (name) => {
this.setState({
[name]: !this.state[name]
})
}
onClick={()=> this.toggleDisplay('showOne')}
Worked Tyvm

Wrong order of selectedItem while using onChange in a dropdown & using material-ui

Well, I have a dropdown and I want to use the onChange() selectedItem to then call an API and render the output using map.
My code looks something like this:
TaskSideBar.js
const taskAPI = 'https://pokeapi.co/api/v2/';
export default class TaskSideBar extends Component {
constructor(props) {
super(props);
this.state = {
pokeData: [],
pokeIndex: null,
isLoading: false,
error: null,
};
this.handleDropdownChange = this.handleDropdownChange.bind(this);
}
componentDidMount() {
}
handleDropdownChange(e) {
this.setState({selectedValue: e.target.value})
fetch(taskAPI + this.state.selectedValue )
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('something is wrong');
}
})
.then (data => this.setState({ pokeData: data.results, pokeIndex: 0, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
render () {
const { pokeData, pokeIndex, isLoading, error, selectedValue, renderRow } = this.state;
const classes = this.props;
return (
<>
<Tabs tabs={['Reports', 'Graphs', 'Sheets']} className={classes.sidebarTabs}>
<>
<h3>Reports</h3>
<List source={reportItems} />
</>
<>
<h3>Graphs</h3>
<Dropdown source={graphItems} />
</>
<>
<h3>Sheets</h3>
<select id="dropdown" onChange={this.handleDropdownChange} className={classes.taskList}>
<option value="">select </option>
<option value="berry">Pokemon Berry</option>
<option value="ability">Pokemon Abilities</option>
<option value="version">Version Info</option>
</select>
</>
</Tabs>
<div>
selected sheet is: {this.state.selectedValue}
{
pokeData.map(hit =>
<div key={hit.name}>
<p> {hit.name} {hit.url} </p>
</div>
)
}
</div>
</>
);
}
}
What is actually happening is, once the page renders and I select 'berry', I get an error: 'https://pokeapi.co/api/v2/undefined': 404. So that means for some reason, the selectedValue was not set to 'berry'. However, if I then go on and select 'ability', it renders the pokeData.map but shows me the results for 'https://pokeapi.co/api/v2/berry' when it should be showing me the data for 'https://pokeapi.co/api/v2/ability' and this keeps happening on each select. It seems like the index is off by -1. I have a follow-up question as well, but I'd appreciate if someone can help me understand this. Thanks.
So, i have a couple of problems here:
Fix the issue with the index on the selectedItem, which is undef for the first selection and is set to index-1 on the next selections.
Perform the same thing using dropdown using material-ui. In which I do something like this:
const dropdownItems: [
'item1',
'item2',
'item3',
];
and the dropdown looks like:
<dropdown> source={dropdownItems} onChange={this.handleDropdownChange} </dropdown>
How do i make this work? It doesn't work as shown above.
The problem is that setState is async. The value is not yet set when you call the fetch and the previous value will be used. For the first time that's undefined.
Either use the value directly in your call from the event or use the setState callback as second parameter to trigger the API call like this.
this.setState({selectedValue: e.target.value}, () => fetch(...)...)
It should also work the same for the drop down.
Hope this helps.
The setState method is async which means that you cannot guarantee it has finished before the fetch method is called and hence the value can sometimes be undefined.
fetch(taskAPI + this.state.selectedValue)
A simple solution would be to use e.target.value in the fetch request too or supply a callback function to the setState that fetches the information once the state has been set.
For more information on setState see - https://reactjs.org/docs/react-component.html#setstate
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
Hope the above clears up the issue!
~Fraz

ag-grid react cellRendererFramework doesn't re render when state changed

I'm displaying data in ag-grid-react and grid has come conditional cell rendering based on state. Here is my code. This is working on first run and displaying "YEAH" button. when i clicked button i want to change with NOOO button
This is state
this.changebutton = this.changebutton.bind(this);
this.state= {
isyes = "yes"
}
This is ag-grid-cell-renderer
cellRendererFramework: (params) => {
return <div>
{
this.state.isyes === "yes" ?
<button onClick= {() => this.changebutton()}>YEAH</button>
:
<button onClick= {() => this.changebutton()}>NOOOO</button>
}
</div>
}
this is state changer
changebutton() {
this.setState({isyes: "no" })
console.log(this.state.isyes)
}
I seeing state is changing properly, But doesn't see any change of button. why?
Your code seems incomplete to check your situation. First thing that comes in mind is the expression is evaluated once (maybe, as it appears as a method of an object not directly in render function) and is never retriggered.
Also notiche that setState() is async so you should not:
this.setState({isyes: "no" });
console.log(this.state.isyes);
instead you should:
this.setState(
{isyes: "no" },
() => console.log(this.state.isyes);
);
Try with:
api.refreshCells()
ref: ag-grid.com/javascript-grid-cell-rendering-components

Resources