React object property setting broken - javascript

I think I found a react bug?
I have 2 functions to show my modal, first I have a modal state like this:
state = {
modal: {
requestPopUp: {
visible: false,
},
},
};
and 2 functions to hide/show it
// Helper Methods
handleOpenModal = name => {
const modal = { ...this.state.modal };
modal[name].visible = true;
this.setState({ modal });
};
handleCloseModal = name => {
const modal = { ...this.state.modal };
modal[name].visible = false;
this.setState({ modal });
};
handleOpenModal works fine, however, handleCloseModal does not, it runs but doesnt alter the state.
modal[name].visible = false; This line specifically does nothing, I logged (modal,name) after it and it stays true and not false, how can I fix this??
Edit: I call my functions here:
<div className="card request p-3" onClick={() => this.handleOpenModal("requestPopUp")}>
Which works fine
and here
<Modal show={modal.requestPopUp.visible} onHide={() => this.handleCloseModal("requestPopUp")}>
Which also calls the function properly but it's never set as false as per the function logic.
My react version is "react": "^16.12.0"

Try to avoid mutating the object props directly e.g (obj[prop] = value) since its antI-pattern ... Use destructering as my example below:
On a different note, no need to write the code twice, you can reuse the same function, and pass an extra param to define if u wanna close/open:
handleModalClick = (name, visible) => {
this.setState({
modal: {
...this.state.modal,
[name]: {...this.state.modal[name], visible }
}
})
}

Related

React Infinite re-render while passing setState hook function to a child component

I'm trying to update my state. I have declared :
const [s_groupes, setGroupes] = useState(
initialGroupes.map(groupe => {
return Object.assign({
name: groupe,
value: true
})
})
)
From an array, and I wish to update this state when I click on a child component.
The problem is I don't know how to pass the function setGroupes to a child component.
I have no problem using onClick={setGroupes} on a button inside the same file, but I don't understand how to use it with props like this :
<PieChart
data={s_groupes}
disableGroupe={click => disableGroupe(click)}
/>
where my function disableGroupe is :
const disableGroupe = click => {
let value
const groupeClicked = s_groupes.find(groupe => {
value = !groupe.value
return groupe.name === click.data.value.name
})
const newGroupes = s_groupes.map(groupe => {
if (groupe.name === groupeClicked.name) {
return { ...groupe, value }
} else {
return { ...groupe }
}
})
setGroupes(newGroupes)
}
It seems to trigger too many renders but I don't get why. Also from consoles.logs it seems it "reset" my state every render.
I must add I'm using on("click", e => return props.disableGroupe(e)) from the D3JS Library in the child component PieChart , the e returns actually something with data.value.name inside (but I'm not sure it's relevant)

Why is the initial state being used when I try to update state

I have a react component that uses hooks for state. I have set the initial state for home to {location:null, canCharge: 'yes'}.
I then have a couple of subcomponents that call setHome() to update the pieces of the state they are responsible for.
One sets the location, and the other sets the canCharge property of the home state.
The setter for the ChargeRadioGroup works as expected, only updating the canCharge property and has no effect on the value of location.
The PlacesAutoComplete set however seems to have captured the initial state of home, and after setting a breakpoint inside, I see that it always is called with home: {location:null, canCharge:'yes'}.
I realize I could break this single state into two separate states, one for location and one for canCharge, but I'd like to understand why this is happening instead of implementing a workaround.
export default function VerticalLinearStepper() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [home, setHome] = useState({
location: null,
canCharge: "yes"
});
const [work, setWork] = useState({
location: null,
canCharge: "yes"
});
const steps = getSteps();
const handleNext = () => {
setActiveStep(prevActiveStep => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep(prevActiveStep => prevActiveStep - 1);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepLabel>Where do you live?</StepLabel>
<StepContent>
<Box className={classes.stepContent}>
<PlacesAutocomplete
className={classes.formElement}
name={"Home"}
onPlaceSelected={location => setHome({ ...home, location })}
googleApiKey={"<API_KEY>"}
/>
<ChargeRadioGroup
className={classes.formElement}
label="Can you charge your car here?"
value={home.canCharge}
onChange={event =>
setHome({ ...home, canCharge: event.target.value })
}
/>
The code for the PlacesAutoComplete component can be seen here
I'm guessing this has something to do with the way that this component calls it's onPlaceSelected prop, but I can't figure out exactly what's going on, or how to fix it:
useEffect(() => {
if (!loaded) return;
const config = {
types,
bounds,
fields
};
if (componentRestrictions) {
config.componentRestrictions = componentRestrictions;
}
autocomplete = new window.google.maps.places.Autocomplete(
inputRef.current,
config
);
event = autocomplete.addListener("place_changed", onSelected);
return () => event && event.remove();
}, [loaded]);
const onSelected = () => {
if (onPlaceSelected && autocomplete) {
onPlaceSelected(autocomplete.getPlace());
}
};
Updating my original answer.
Instead of this:
onPlaceSelected={location => setHome({ ...home, location })}
This:
onPlaceSelected={newlocation => setHome( (prevState) => (
{ ...prevState, location:newlocation }
))}
The set state functions can take a value, and object or a function that receives the old state and returns the new state. Because setting state is sometimes asynchronous, object state with members getting set with different calls may result in captured variables overwriting new state.
More details at this link: https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3

How to toggle state in React Native

I am developing a light weight project using React Native, and I encountered some setbacks, I couldn't figure it out. :(
I have a page that contains a Yes and a No button and a Yes/No render area, users will be able to click on either of the buttons. According to the users' choice, an avatar will appear in the correct render area (click yes, the avatar will be in the Yes area...). But one user can only be able to click once. I am trying to solve this using state and setState, but couldn't get it to work.
I have:
this.state = {invitedState : false}
and a function (part)
onPress={() => {
if (this.state.invitedState) {
onPress();
}
this.setState(prevState => ({
invitedState: !prevState.invitedState,
}));
}}
Should I not use setState to solve this problem?
thanks!
I think I understand your problem. Something like this?
state = {
toggleUI: true,
userToggled: false
};
handleToggleUI = e => {
this.setState(currentState => {
if ( this.state.userToggled === false ) {
return {
toggleUI: !currentState.toggleUI,
userToggled: true
};
}
});
};
You could try:
onPress{() => {
let tempVar = this.state.invitedState ? false : true;
this.setState({invitedState: tempVar});
}

I fail to setState my ReactJS state dynamically with a function

I'm trying to set the display of my ReactJS component dynamically.
Basically the game is the following : the user push a button, then the value of the affiliate state is set to true. Allowing to handle the displaying of the buttons.
However my state doesn't changes when I push a button, despite I log it after the change would have occurred. However I have set my state. I wonder what going wrong.
Here my tiny snippet, easy testable and reproducible :
https://codesandbox.io/s/21963yy01y
Here my snippet.js :
export default class App extends React.Component {
state = {
displaySelection: false,
displayCreate: false,
displayUpdate: false,
displayDelete: false,
}
viewSelection = (e) => {
e.preventDefault();
Object.keys(this.state).map((key, index) => {
// console.log("key value: ", key)
console.log("target name: ", e.target.name)
if (key === e.target.name) {
console.log(e.target.name, " set to true =)")
return this.setState({ [e.target.name]: true }, console.log("display state: ", this.state))
} else {
this.setState({ [e.target.name]: false })
}
});
}
render() {
return (
<div className="App">
<button onClick={this.viewSelection}> Launch the Object.keys function =) splay</button>
<div >
<button name="displayCreate" onClick={this.viewSelection}> Create </button>
<button name="displayUpdate" onClick={this.viewSelection}> Update </button>
<button name="displayDelete" onClick={this.viewSelection}> Delete </button>
<button name="displaySelection" onClick={this.viewSelection}> O </button>
</div>
</div>
);
}
}
Why when I push a button the state of this button doesn't change ?
Any hint would be great,
thanks
Found a flaw in your logic. In your else statement in your viewSelection function, you have:
else {
this.setState({ [e.target.name]: false });
}
So in every iteration of the loop, you are setting the target that was clicked to false in state. You can solve that by changing e.target.name to key, like so:
else {
this.setState({ [key]: false });
}
So that way you're only changing the key that isn't the current target. But this is still inefficient because you're still running setState 4 times (1 for each key in state). One more efficient way to achieve what you're looking for is to have an object (essentially a copy of what's in state) with the keys set to false by default. Then take the target from the click and set that to true, like so:
viewSelection = e => {
e.preventDefault();
let newValues = {
displaySelection: false,
displayCreate: false,
displayUpdate: false,
displayDelete: false
};
newValues[e.target.name] = true;
this.setState(newValues);
}

React this.setState not updating my value, on firebase changes are showing

I have a problem with my code. I am trying to make a notes in react + fireabse. Adding notes to fireabse works and setState shows them, but if I want to change the value of the note, second setState does not change it but in firebase the note will change its value.
Here is my code
constructor() {
super();
this.app = firebase.initializeApp(DB_CONFIG);
this.database = this.app.database().ref().child('notes');
this.state = {
notes: [],
};
}
componentDidMount() {
this.database.on('child_added', snap => {
this.state.notes.push(new Note(snap.key, snap.val().noteContent));
this.setState({
notes: this.state.notes
});
});
this.database.on('child_changed', snap => {
this.state.notes.forEach(note => {
if(snap.key === note.id) {
note.id = snap.key;
note.noteContent = snap.val().noteContent;
}
});
this.setState({
notes: this.state.notes,
});
});
}
addNote(note) {
this.database.push().set({
noteContent: note,
});
}
changeNote(id, note) {
this.database.child(id).update({
noteContent: note,
});
}
render() {
return (
<div>
<div> {
this.state.notes.map(note => {
return (
<NoteComponent noteContent={note.noteContent}
noteId={note.id}
key={note.id}
changeNote={this.changeNote.bind(this)}>
</NoteComponent>
)
})
}
</div>
<div>
</div>
<NoteFormComponent
addNote={this.addNote.bind(this)}>
</NoteFormComponent>
</div>
);
}
Thanks for help.
Problem lines:
this.state.notes.push(new Note(snap.key, snap.val().noteContent));
or
this.state.notes.forEach(note => {
if(snap.key === note.id) {
note.id = snap.key;
note.noteContent = snap.val().noteContent;
}
});
this.setState({
notes: this.state.notes,
});
You cannot change the value of the state like this. You HAVE to use setState.
To fix this you need to:
copy(deepclone) the state array (You shoudl use ImmutableJS but this will work for testing: const copiedArray = JSON.parse(JSON.stringify(array)))
Do the changes to the copied array copiedArray.
setState('notes', copiedArray)
Other suggestions:
I would suggest to you to do the following. Isolate the firebase layer from the viewing layer. Mixing responsibilities of the component with the db communication is not recommanded.
After you do that. you will pass the list of note from outside the component. And any method working on the database(firebase in you case will come as a param also.)
// Notes Container
const notes = FireBaseConnector.getNotes();
const {
add,
edit,
delete,
} = FireBaseConnector
<Notes notes={notes}
onAdd={add}
onEdit={edit}
onRemove={delete}
/>
You should do something like this:
const newNotes = [...this.state.notes]
const newNode = {
id: snap.key,
noteContent: snap.val().noteContent,
}
newNotes.push(newNode)
this.setState({
notes: newNotes,
});
you cannot just push you need to replace the node array with the new array. immutability need to be followed to tell react to rerender

Categories