multiple react state updates renders only the last update - javascript

I made a little function to add custom alerts to the state array, and the view renders them. The problem is, if i call the function twice in a row, the first alert is rendered only for a moment, and then its replaced with the second alert. When i call the method with a mouse click, the function works correctly.
I have tried to apply some waiting before pushing to the array list, but no luck with that.
const Element = () => {
const [alerts, setAlerts] = React.useState([])
const addAlert = (data) => {
setAlerts([...alerts, <CustomAlert key={alerts.length} message={data.message} color={data.color} />])
}
return (
<div>
<button onClick={() => {
// this renders only the last state update.
addAlert({message: "test", color: "error"});
addAlert({message: "2", color: "error"})
}
}>
add alert button
</button>
<div>
{alerts}
</div>
</div>
);
}

React updates the state asynchronously. This means when you are updating the state 2 times in a row, accessing the value of alerts directly might not have the latest inserted item. You should use a function instead when calling setAlerts:
const [alerts, setAlerts] = React.useState([]);
const addAlert = (data) => {
setAlerts((prevAlerts) => {
const newAlerts = [...prevAlerts];
newAlerts.push(
<CustomAlert
key={alerts.length}
message={data.message}
color={data.color}
/>
);
return newAlerts;
});
};
return (
<div>
<button
onClick={() => {
// this renders only the last state update.
addAlert({ message: "test", color: "error" });
addAlert({ message: "2", color: "error" });
}}
>
add alert button
</button>
</div>
);

alerts in your code refers to the value of the current render in both case so your addAlert won't work. To fix this, you can use the setter version with a function:
setAlerts(currentAlerts => [...currentAlters, <CustomAlert key={alerts.length} message={data.message} color={data.color} />])

Related

State in child component is not getting updated after button click

I was trying to create a delete operation on the array of objects (videoData).videoData is getting mapped in the child component along with the DELETE button. At the click of the DELETE button in the child component (childComp).
I want to set the current id to the "childData" state in the child component but it's not getting updated with the current id.
When I am consoling log the childData, in the child component, it still says null which means it was not updated.Why is it not updating?
My own explanation -
When the delete button is getting clicked, the testFunc() is getting fired in the parent component which is removing the item with that particular id from videoData array, and as a result, the id is not able to pass to the child component due to which child component is getting rendered with the original state (null). I don't know if the explanation is correct or not, can someone help me in clearing this up?
function ParentComp() {
const [videoData, setvideoData] = useState([{ id: 2 }, { id: 3 }]);
function testFunc(id) {
let hasMatch = false
if (!hasMatch) {
let arr = videoData.filter(item => {
return item.id !== id
})
setvideoData(arr)
}
}
return (
<childComp testFunc={testFunc} videoData={videoData}/>
)
}
function childComp({testFunc, videoData}) {
const [childData, setchildData] = useState(null)
function ChildFunc(itemId) {
testFunc(itemId)
setchildData(itemId)
}
console.log(childData) //null (state not getting updated)
return (
<>
{videoData.map((item) => {
return (
<button onClick={() => ChildFunc(item.id)}>Delete</button>
);
})}
</>
)
}

React | Adding and deleting object in React Hooks (useState)

How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};

how to fix form submission with useEffect hook (as is: need to click submit twice)

App takes user options and creates an array objects randomly, and based on user options. (it's a gamer tag generator, writing to learn react.js). As is, App is a functional component and I use useState to store array of objects (gamertags) and the current selected options.
I use formik for my simple form. It takes two clicks to get a new item with updated options. I know why, options in state of App doesn't not update until it rerenders as the function for form submission is async. Therefore, all of my options are updated, after the first click, and are correct with the second because they were updated with the rerendering and after I needed them.
I know the solution is to use a useEffect hook, but despite reading over other posts and tuts, I don't understand how to apply it. It's my first instance of needing that hook and I'm still learning.
I wrote a simplified App to isolate the problem as much as possible and debug. https://codesandbox.io/s/morning-waterfall-impg3?file=/src/App.js
export default function App() {
const [itemInventory, setItemInventory] = useState([
{ options: "apples", timeStamp: 123412 },
{ options: "oranges", timeStamp: 123413 }
]);
const [options, setOptions] = useState("apples");
const addItem = (item) => {
setItemInventory([item, ...itemInventory]);
};
const createItem = () => {
return { options: options, timeStamp: Date.now() };
};
class DisplayItem extends React.Component {
render() { // redacted for brevity}
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
addItem(createItem());
};
const UserForm = (props) => {
return (
<div>
<Formik
initialValues={{
options: props.options
}}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
console.log(values);
props.onUpdate(values);
}}
>
{({ values }) => (
<Form> //redacted for brevity
</Form>
)}
</Formik>
</div>
);
};
return (
<div className="App">
<div className="App-left">
<UserForm options={options} onUpdate={onFormUpdate} />
</div>
<div className="App-right">
{itemInventory.map((item) => (
<DisplayItem item={item} key={item.timeStamp} />
))}
</div>
</div>
);
}
This is probably a "layup" for you all, can you help me dunk this one? Thx!
Solved problem by implementing the useEffect hook.
Solution: The functions that create and add an item to the list, addItem(createItem()), become the first argument for the useEffect hook. The second argument is the option stored in state, [options]. The callback for the form, onFormUpdate only updates the option in state and no longer tries to alter state, i.e. create and add an item to the list. The useEffect 'triggers' the creation and addition of a new item, this time based on the updated option because the updated option is the second argument of the hook.
Relevant new code:
useEffect( () => {
addItem(createItem());
}, [options]);
const onFormUpdate = (values) => {
const newOption = values.options;
setOptions(newOption);
//addItem used to be here
};

Rendering arrays using .map that contains state variables - not updating

I have an array that I am rendering on the render() function. Each element in the array is a HTML element that has state variables that I need to display, the HTML are displaying correctly, but the internal state variables do not update even when the rendering is happening
state = {
array: [],
id: 2
}
updateState() {
this.setState({id: 4})
}
componentDidMount(){
array = [<div> {this.state.id} </div>, <div> {this.state.id} </div>]
}
render() {
{this.state.array.map(el => return el)}
//assume something happens here that triggers updateState() multiple times: buttons presses, etc
}
I never see 4, it re renders but keeps the old value 2
You are creating the array in the componentDidMount function which is only being called once when the component first renders.
You should do something like this
//create function
createArray = () => [<div> {this.state.id} </div>, <div> {this.state.id} </div>]
then use it in your code like this
{this.createArray().map(el => el)}
Hope this helps.
You need to save the data and render again:
state = {
id: 2
}
updateState() {
this.setState({id: 4})
}
componentDidMount(){
this.getElements(this.state.id)
}
getElements = (id) => {
return [<div> {id} </div>, <div> {id} </div>]
}
render() {
{this.getElements(this.state.id).map(el => el)}
//assume something happens here that triggers updateState() multiple times: buttons presses, etc
}

React - change this.state onClick rendered with array.map()

I'm new to React and JavaScript.
I have a Menu component which renders an animation onClick and then redirects the app to another route, /coffee.
I would like to pass the value which was clicked (selected) to function this.gotoCoffee and update this.state.select, but I don't know how, since I am mapping all items in this.state.coffees in the same onClick event.
How do I do this and update this.state.select to the clicked value?
My code:
class Menus extends Component{
constructor (props) {
super(props);
this.state = {
coffees:[],
select: '',
isLoading: false,
redirect: false
};
};
gotoCoffee = () => {
this.setState({isLoading:true})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
renderCoffee = () => {
if (this.state.redirect) {
return (<Redirect to={`/coffee/${this.state.select}`} />)
}
}
render(){
const data = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{data.map(c =>
<span key={c}>
<div>
{this.state.isLoading && <Brewing />}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee()}
<strong><font color="#C86428">{c}</font></strong></div>
</div>
</span>)
}
</div>
);
}
}
export default withRouter(Menus);
I have tried passing the value like so:
gotoCoffee = (e) => {
this.setState({isLoading:true,select:e})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
console.log(this.state.select)
}
an like so:
<div onClick={(c) => this.gotoCoffee(c)}
or so:
<div onClick={(event => this.gotoCoffee(event.target.value}
but console.log(this.state.select) shows me 'undefined' for both tries.
It appears that I'm passing the Class with 'c'.
browser shows me precisely that on the uri at redirect:
http://localhost/coffee/[object%20Object]
Now if I pass mapped 'c' to {this.renderCoffee(c)}, which not an onClick event, I manage to pass the array items.
But I need to pass not the object, but the clicked value 'c' to this.gotoCoffee(c), and THEN update this.state.select.
How do I fix this?
You can pass index of element to gotoCoffee with closure in render. Then in gotoCoffee, just access that element as this.state.coffees[index].
gotoCoffee = (index) => {
this.setState({isLoading:true, select: this.state.coffees[index]})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
render(){
const data = this.state.coffees;
return (
<div>
<h1 className="title is-1"><font color="#C86428">Menu</font></h1>
<hr/><br/>
{data.map((c, index) =>
<span key={c}>
<div>
{this.state.isLoading && <Brewing />}
{this.renderCoffee()}
<div onClick={() => this.gotoCoffee(index)}
<strong><font color="#C86428">{c}</font></strong></div>
</div>
</span>)
}
</div>
);
}
}
so based off your code you could do it a couple of ways.
onClick=(event) => this.gotoCoffee(event.target.value)
This looks like the approach you want.
onClick=() => this.gotoCoffee(c)
c would be related to your item in the array.
All the answers look alright and working for you and it's obvious you made a mistake by not passing the correct value in click handler. But since you're new in this era I thought it's better to change your implementation this way:
It's not necessary use constructor at all and you can declare a state property with initial values:
class Menus extends Component{
state= {
/* state properties */
};
}
When you declare functions in render method it always creates a new one each rendering which has some cost and is not optimized. It's better if you use currying:
handleClick = selected => () => { /* handle click */ }
render () {
// ...
coffees.map( coffee =>
// ...
<div onClick={ this.handleClick(coffee) }>
// ...
}
You can redirect with history.replace since you wrapped your component with withRouterand that's helpful here cause you redirecting on click and get rid of renderCoffee method:
handleClick = selected => () =>
this.setState(
{ isLoading: true},
() => setTimeout(
() => {
const { history } = this.props;
this.setState({ isLoading: false });
history.replace(`/${coffee}`);
}
, 5000)
);
Since Redirect replaces route and I think you want normal page change not replacing I suggest using history.push instead.
You've actually almost got it in your question. I'm betting the reason your state is undefined is due to the short lived nature of event. setState is an asynchronous action and does not always occur immediately. By passing the event off directly and allowing the function to proceed as normal, the event is released before state can be set. My advice would be to update your gotoCoffee function to this:
gotoCoffee = (e) => {
const selectedCoffee = e.target.value
this.setState({isLoading:true,select:selectedCoffee},() =>
{console.log(this.state.select})
setTimeout(()=>{
this.setState({isLoading:false,redirect:true})
},5000)
}
Note that I moved your console.log line to a callback function within setState so that it's not triggered until AFTER state has updated. Any time you are using a class component and need to do something immediately after updating state, use the callback function.

Categories

Resources