Inside of a function the state is always same/initial - javascript

The remove() function is called from an object. How can I get updated state value inside of that remove() function.
const [InfoBoxPin, setInfoBoxPin] = useState([])
const createInfoBoxPin = (descriptions) =>{
var newPin = {
"location":currentLoc,
"addHandler":"mouseover",
"infoboxOption": {
title: 'Comment',
description: "No comment Added",
actions: [{
label:'Remove Pin',
eventHandler: function () {
remove(newPin.location) //FUNCTION CALLED HERE
}
}] }
}
setInfoBoxPin((InfoBoxPin)=>[...InfoBoxPin, newPin ]) // UPDATE STATE. Push the above object.
}
const remove = (pos) =>{
console.log(InfoBoxPin) //NEVER GETTING UPDATED STATE HERE.
//Other codes here......
}
This is a bing map Info card. Eventhandler creates a button which can call any function.

The problem is that you are referring to old state information in the remove function.
When you call setInfoBoxPin - the state of InfoBoxPin is registered for an update on the next render of UI. This means that in current state it will be the same (empty) and all links to it will refer to an empty array.
In order to fix this, you will have to pass your new state to appropriate functions from the View itself.
Example #1
Here, I have created a CodeSandBox for you:
https://codesandbox.io/s/react-setstate-example-4d5eg?file=/src/App.js
And here is the code snipped from it:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [state, setState] = useState({
InfoBoxPin: [],
output: []
});
const createInfoBoxPin = (descriptions) => {
var newPin = {
location: Math.round(Math.random(10) * 1000),
addHandler: "mouseover",
infoboxOption: {
title: "Comment",
description: "No comment Added",
actions: [
{
label: "Remove Pin",
eventHandler: removePin
},
{
label: "Update Pin",
eventHandler: updatePin
}
]
}
};
setState({ ...state, InfoBoxPin: [...state.InfoBoxPin, newPin] });
};
const updatePin = (key, state) => {
var text = `Updating pin with key #${key} - ${state.InfoBoxPin[key].location}`;
setState({ ...state, output: [...state.output, text] });
console.log(text, state.InfoBoxPin);
};
const removePin = (key, state) => {
var text = `Removing pin with key #${key} - ${state.InfoBoxPin[key].location}`;
setState({ ...state, output: [...state.output, text] });
console.log(text, state.InfoBoxPin);
};
return (
<div className="App">
<h1>React setState Example</h1>
<h2>Click on a button to add new Pin</h2>
<button onClick={createInfoBoxPin}>Add new Pin</button>
<div>----</div>
{state.InfoBoxPin.map((pin, pin_key) => {
return (
<div key={pin_key}>
<span>Pin: {pin.location} </span>
{pin.infoboxOption.actions.map((action, action_key) => {
return (
<button
key={action_key}
onClick={() => action.eventHandler(pin_key, state)}
>
{action.label}
</button>
);
})}
</div>
);
})}
<h4> OUTPUT </h4>
<ul style={{ textAlign: "left" }}>
{state.output.map((txt, i) => {
return <li key={i}>{txt}</li>;
})}
</ul>
</div>
);
}
As you can see I am providing a new state with InfoBoxPin value to a function named eventHandler for onclick event listener of a button.
And then in that function, I can use the new InfoBoxPin value from state how I need it.
Example #2 (ES6)
In this example, I am using a bit different structure for App - using class (ES6)
By using a class for App, we can manipulate App state using different methods.
func.bind(this) can be used on defined function on initialization
func.call(this) can be used to call a dynamic function without arguments
func.apply(this, [args]) can be used to call a dynamic function with arguments
CodeSandBox Link:
https://codesandbox.io/s/react-setstate-example-using-class-cz2u4?file=/src/App.js
Code Snippet:
import React from "react";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
InfoBoxPin: [],
pinName: ""
};
/* ------------ method #1 using .bind(this) ------------ */
this.setPinName = this.setPinName.bind(this);
}
remove(key) {
this.state.InfoBoxPin.splice(key, 1);
this.setState({ InfoBoxPin: this.state.InfoBoxPin });
}
add(pinName) {
this.state.InfoBoxPin.push(pinName);
this.setState({ InfoBoxPin: this.state.InfoBoxPin });
}
processPinNameAndAdd() {
let pinName = this.state.pinName.trim();
if (pinName === "") pinName = Math.round(Math.random() * 1000);
this.add(pinName);
}
setPinName(event) {
this.setState({ pinName: event.target.value });
}
render() {
return (
<div className="shopping-list">
<h1>Pin List</h1>
<p>Hit "Add New Pin" button.</p>
<p>(Optional) Provide your own name for the pin</p>
<input
onInput={this.setPinName}
value={this.state.pinName}
placeholder="Custom name"
></input>
{/* ------------ method #2 using .call(this) ------------ */}
<button onClick={() => this.processPinNameAndAdd.call(this)}>
Add new Pin
</button>
<ul>
{this.state.InfoBoxPin.map((pin, pinKey) => {
return (
<li key={pinKey}>
<div>pin: {pin}</div>
{/* ------------ method #3 using .apply(this, [args]) ------------ */}
<button onClick={() => this.remove.apply(this, [pinKey])}>
Delete Pin
</button>
</li>
);
})}
</ul>
</div>
);
}
}
export default App;
Example #3 (ES6) without access to the created element
This example will show how to handle callbacks from third-party libraries with our own arguments and state data from the event of an auto-generated HTML element
CodeSandBox Link:
https://codesandbox.io/s/react-setstate-example-using-class-no-element-control-lcz5d?file=/src/App.js
Code Snippet:
import React from "react";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
InfoBoxPin: [],
lastPinId: 0,
pinName: ""
};
this.setPinName = this.setPinName.bind(this);
}
remove(id) {
let keyToRemove = null;
this.state.InfoBoxPin.forEach((pin, key) => {
if (pin.id === id) keyToRemove = key;
});
this.state.InfoBoxPin.splice(keyToRemove, 1);
this.setState({ InfoBoxPin: this.state.InfoBoxPin });
}
add(data, id) {
this.state.InfoBoxPin.push({ id: id, data: data });
this.setState({
InfoBoxPin: this.state.InfoBoxPin,
lastPinId: id
});
}
processPinNameAndAdd() {
let pinName = this.state.pinName.trim();
if (pinName === "") pinName = Math.round(Math.random() * 1000);
var newPinId = this.state.lastPinId + 1;
var newPin = {
location: pinName,
addHandler: "mouseover",
infoboxOption: {
title: "Comment",
description: "No comment Added",
actions: [
{
label: "Remove Pin #" + newPinId,
// [ES6 class only] using () => func() for callback function
// By doing so we don't need to use bind,call,apply to pass class ref [this] to a function.
eventHandler: () => this.remove(newPinId)
}
]
}
};
this.add(newPin, newPinId);
}
setPinName(event) {
this.setState({ pinName: event.target.value });
}
render() {
return (
<div className="shopping-list">
<h1>Pin List</h1>
<p>Hit "Add New Pin" button.</p>
<p>(Optional) Provide your own name for the pin</p>
<input onInput={this.setPinName} value={this.state.pinName}></input>
{/*
[ES6 class only] Using {() => func()} for event handler.
By doing so we don't need to use func.bind(this) for passing class ref at constructor
*/}
<button onClick={() => this.processPinNameAndAdd()}>Add new Pin</button>
<ul>
{this.state.InfoBoxPin.map((pin, pKey) => {
return (
<li key={pKey}>
<div>pin: {pin.data.location}</div>
{pin.data.infoboxOption.actions.map((action, aKey) => {
return (
<button key={aKey} onClick={action.eventHandler}>
{action.label}
</button>
);
})}
</li>
);
})}
</ul>
</div>
);
}
}
export default App;
I have created lastPinId entry in a State to track newly created Pin's ids.
Pin id can be used later to find the desired pin in the InfoBoxPin collection for removal.
The most important part how to register your eventHandler is this:
eventHandler: () => this.remove(newPinId)
Please note that using arrow function () => func is important to pass class ref to remove function.

Related

React Context API - get updated state value

I am experimenting with React context api,
Please check someComponent function where I am passing click event (updateName function) then state.name value update from GlobalProvider function
after updated state.name it will reflect on browser but not getting updated value in console ( I have called console below the line of click function to get updated value below )
Why not getting updated value in that console, but it is getting inside render (on browser) ?
Example code
App function
<GlobalProvider>
<Router>
<ReactRouter />
</Router>
</GlobalProvider>
=== 2
class GlobalProvider extends React.Component {
state = {
name: "Batman"
};
render() {
return (
<globalContext.Provider
value={{
name: this.state.name,
clickme: () => { this.setState({ name: "Batman 2 " }) }
}}
>
{this.props.children}
</globalContext.Provider>
);
}
}
export default GlobalProvider;
=== 3
const SomeComponent = () => {
const globalValue = useContext(globalContext);
const updateName = ()=> {
globalValue.clickme();
console.log(globalValue.name ) //*** Here is my concern - not getting updated value here but , getting updated value in browser
}
return (
<div onClick={(e)=> updateName(e) }>
{globalValue.name}//*** In initial load display - Batman, after click it display Batman 2
</div>) }
React state isn't an observer like Vue or Angular states which means you can't get updated values exactly right after changing them.
If you want to get the updated value after changing them you can follow this solution:
class A extends Component {
state = {
name: "Test"
}
updateName = () => {
this.setState({name: "Test 2"}, () => {
console.log(this.state.name) // here, name has been updated and will return Test 2
})
}
}
So, you need to write a callback function for the clickme and call it as below:
class GlobalProvider extends React.Component {
state = {
name: "Batman"
};
render() {
return (
<globalContext.Provider
value={{
name: this.state.name,
clickme: (callback) => { this.setState({ name: "Batman 2 " }, () => callback(this.state.name)) }
}}
>
{this.props.children}
</globalContext.Provider>
);
}
}
export default GlobalProvider;
And for using:
const SomeComponent = () => {
const globalValue = useContext(globalContext);
const updateName = ()=> {
globalValue.clickme((name) => {
console.log(name) // Batman 2
});
}
return (
<div onClick={(e)=> updateName(e) }>
{globalValue.name}//*** In initial load display - Batman, after click it display Batman 2
</div>)
}

Why can't I add any value to the array in state?

I have a lot of hits, which I want to add to an array once a hit is pressed. However, as far as I observed, the array looked like it got the name of the hit, which is the value. The value was gone in like half second.
I have tried the methods like building constructor, and doing things like
onClick={e => this.handleSelect(e)}
value={hit.name}
onClick={this.handleSelect.bind(this)}
value={hit.name}
onClick={this.handleSelect.bind(this)}
defaultValue={hit.name}
and so on
export default class Tagsearch extends Component {
constructor(props) {
super(props);
this.state = {
dropDownOpen:false,
text:"",
tags:[]
};
this.handleRemoveItem = this.handleRemoveItem.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.handleTextChange = this.handleTextChange.bind(this);
}
handleSelect = (e) => {
this.setState(
{ tags:[...this.state.tags, e.target.value]
});
}
render() {
const HitComponent = ({ hit }) => {
return (
<div className="infos">
<button
className="d-inline-flex p-2"
onClick={e => this.handleSelect(e)}
value={hit.name}
>
<Highlight attribute="name" hit={hit} />
</button>
</div>
);
}
const MyHits = connectHits(({ hits }) => {
const hs = hits.map(hit => <HitComponent key={hit.objectID} hit={hit}/>);
return <div id="hits">{hs}</div>;
})
return (
<InstantSearch
appId="JZR96HCCHL"
apiKey="b6fb26478563473aa77c0930824eb913"
indexName="tags"
>
<CustomSearchBox />
{result}
</InstantSearch>
)
}
}
Basically, what I want is to pass the name of the hit component to handleSelect method once the corresponding button is pressed.
You can simply pass the hit.name value into the arrow function.
Full working code example (simple paste into codesandbox.io):
import React from "react";
import ReactDOM from "react-dom";
const HitComponent = ({ hit, handleSelect }) => {
return <button onClick={() => handleSelect(hit)}>{hit.name}</button>;
};
class Tagsearch extends React.Component {
constructor(props) {
super(props);
this.state = {
tags: []
};
}
handleSelect = value => {
this.setState(prevState => {
return { tags: [...prevState.tags, value] };
});
};
render() {
const hitList = this.props.hitList;
return hitList.map(hit => (
<HitComponent key={hit.id} hit={hit} handleSelect={this.handleSelect} />
));
}
}
function App() {
return (
<div className="App">
<Tagsearch
hitList={[
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
{ id: 3, name: "Third" }
]}
/>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
additionally:
note the use of prevState! This is a best practice when modifying state. You can google as to why!
you should define the HitComponent component outside of the render method. it doesn't need to be redefined each time the component is rendered!

React function component setTimeout - Multiple render calls and rerenders (recommended approach) (fires multiple times)

I have a Notification component that should close itself after a few seconds and call the onClose prop:
function Notification(props) {
console.log("Notification function component called");
setTimeout(() => {
props.onClose();
}, 4000);
return (
<div>
{props.children}
<button onClick={props.onClose}>Close</button>
</div>
);
}
In my App, I have a state that holds notifications object and I map through them.
class App extends React.Component {
constructor() {
super();
this.pushNotification = this.pushNotification.bind(this);
}
state = {
notifications: {}
};
pushNotification() {
const id = uuid();
const newNotifications = { ...this.state.notifications };
const date = new Date();
newNotifications[id] = {
id,
date: JSON.stringify(date)
};
this.setState({
notifications: newNotifications
});
}
removeNotification(id) {
console.log("removeNotification");
const newNotifications = { ...this.state.notifications };
delete newNotifications[id];
this.setState({
notifications: newNotifications
});
}
render() {
return (
<div className="App">
<button onClick={this.pushNotification}>Push notification</button>
{Object.keys(this.state.notifications).map(
(notificationIndexKey, index) => {
return (
<Notification
originalKey={JSON.stringify(index)}
key={notificationIndexKey}
onClose={() => {
console.log("Notfication fired on close");
this.removeNotification(notificationIndexKey);
}}
>
Notification{" "}
{this.state.notifications[notificationIndexKey].date}
</Notification>
);
}
)}
</div>
);
}
}
I've noticed that if I push multiple notifications in my state, the setTimout is initialized multiple times (which makes sense since render it's called every time the state is updated)
My question is, how would you recommend optimizing this so that the timeout to be invoked only once.
One method that I've tried is to create an array with items that I've removed and check before I call the prop.
Sandbox here: https://codesandbox.io/s/6y3my2y2jr
You should apply that side-effect when the component has mounted.
Currently your code will do this on render.
The render function can be called multiple times.
This code should reflect the correct changes.
class Notification extends React.Component {
componentDidMount() {
setTimeout(this.props.onClose, 4000);
}
render() {
return (
<div>
{props.children}
<button onClick={props.onClose}>Close</button>
</div>
);
}
}
You can do this by keeping a class property say notificationTimer initially set to null and can modify your Notification functions as:
function Notification(props) {
console.log("Notification function component called");
if (!this.notificationTimer)
this.notificationTimer = setTimeout(() => {
props.onClose();
}, 4000);
}
return (
<div>
{props.children}
<button onClick={props.onClose}>Close</button>
</div>
);
}
And in your close function you can do something like this:
onClose() {
// Your code.
if (this.notificationTimer) {
clearTimeout(this.notificationTimer);
this.notificationTimer = null;
}
}
This will not let you create multiple timers.

Why Is the button returning null values for my event.target in my button?

When I'm clicking the button, I am getting that all the values are undefined for the "name" and the "value". I am not sure why, my binding seems correct.
I've tried changing the bindings, I've tried calling an anonymous function for the onClick and passing in the item within my map function. No luck.
import React, { Component } from 'react'
const starterItems = [{ id: 1, name: 'longsword', enhancement: 4 },
{ id: 2, name: 'gauntlet', enhancement: 9 },
{ id: 3, name: 'wizard\'s staff', enhancement: 14 }];
export default class Items extends Component {
constructor(props) {
super(props)
this.handleScoreChange = this.handleScoreChange.bind(this);
this.state = {
items: starterItems
}
}
handleScoreChange(e){
let { name, value } = e.target;
const id = name;
const newScore = value++;
const items = this.state.items.slice();
items.forEach((item) => {
if (item[id] === name){
item.enhancement = newScore
}
});
this.setState(items);
};
render() {
return (
<div>
<h3 data-testid="title">Items</h3>
{this.state.items.map(item => (
<div key={item.id}>
<div name={item.name}data-testid="item">{item.name}</div>
<div name={item.enhancement}data-testid="enhancement" value=
{item.enhancement}>{item.enhancement}
</div>
<button onClick={this.handleScoreChange}>Enhance</button>
</div>
))}
</div>
);
};
}
I am expecting the value of the item passed through to +1
e.target is the DOM reference for the button
including name and value as attributes for the div are not necessary
If you want to get the values for the current name and enhancement when clicking you can add a binding
{
this.state.items.map(item => {
const onClick = this.handleScoreChange.bind(this, item.name, item.enhancement)
return (
<div key={item.id}>
<div name={item.name}data-testid="item">{item.name}</div>
<div name={item.enhancement}data-testid="enhancement" value={item.enhancement}>{item.enhancement}</div>
<button onClick={onClick}>Enhance</button>
</div>
)
)
}
...
handleScoreChange(name, enhancement) {
// your logic here
}
<button onClick={()=>this.handleScoreChange(...item)}>Enhance</button>
please try this i am sure this will work for you
thanks
This Solution Worked. Slight modification from what Omar mentioned. Thank you
import React, { Component } from 'react'
const starterItems = [{ id: 1, name: 'longsword', enhancement: 4 },
{ id: 2, name: 'gauntlet', enhancement: 9 },
{ id: 3, name: 'wizard\'s staff', enhancement: 14 }];
export default class Items extends Component {
constructor(props) {
super(props)
this.state = {
items: starterItems
}
}
enhanceItem(passedItem) {
const newScore = passedItem.enhancement + 1;
const items = this.state.items.slice(); /* creates a copy of state that we can change */
items.forEach((item) => {
if (item.id === passedItem.id) {
return item.enhancement = newScore
}
});
this.setState(items);
};
render() {
return (
<div>
<h3 data-testid="title">Items</h3>
{
this.state.items.map(item => {
const onClick = this.enhanceItem.bind(this, item)
/* this line above binds the item with the onClick Function */
/* notice below, I had to put a return since I added the above code */
return (
<div key={item.id}>
<div data-testid="item">{item.name}</div>
<div data-testid="enhancement">{item.enhancement}</div>
{/* We can use the "data=testid" as a way to grab an item, since it won't impact the html any */}
<button onClick={onClick}>Enhance</button>
</div>
)
})};
</div>
);
};
}

Click handler for each button incorrectly returns the last button of a set

I am trying to add a click handler to each button that is generated in a loop and inserted into an array.
However, clicking a button always outputs the last button of each row of buttons and not the specific button itself.
My code is rather verbose, but we only need to be looking at the time.push() part and the click handler setup. Everything else is just setup.
import React from 'react';
import { friendlyTimeSlot, scopedTimeslots } from '../../utilities/helpers';
class TimeSlotStack extends React.Component {
constructor() {
super();
this.clickHandler = this.clickHandler.bind(this);
this.state = {
times: undefined
};
}
componentWillMount() {
this.updatePropsAndState(this.props);
}
componentWillReceiveProps(nextProps) {
this.updatePropsAndState(nextProps);
this.forceUpdate();
}
updatePropsAndState(props) {
const time = [];
let matchedTimeSlots;
if (props.promotionId) {
matchedTimeSlots = props.timeSlots.filter(timeSlot => {
const timeSlotsIds = timeSlot.AvailablePromotions.map(p => p.Id);
if (timeSlotsIds.includes(props.promotionId)) {
return timeSlot;
}
return false;
});
} else {
matchedTimeSlots = props.timeSlots.filter(timeSlot => timeSlot.HasStandardAvailability);
}
const scopedTimes = scopedTimeslots(matchedTimeSlots, props.preferredTimeSlot);
scopedTimes.forEach((item, i) => {
const friendlyTime = friendlyTimeSlot(item.TimeSlot, true);
const leaveTimeRequired = item.IsLeaveTimeRequired;
let itemPromo;
let leaveTime;
let itemPrice;
if (props.promotionId) {
itemPromo = item.AvailablePromotions.find(ourItem => ourItem.Id === props.promotionId);
leaveTime = itemPromo.LeaveTime || item.LeaveTime;
itemPrice = (itemPromo.BasePrice > 0) ? `£${itemPromo.BasePrice}` : '';
} else {
leaveTime = item.LeaveTime;
}
time.push(
<button
className="btn btn-default"
type="button"
onClick={(e) => this.clickHandler(e)}
ref={input => {
this.button = input;
}}
key={i}
data-time={friendlyTime}
data-leave-time-required={leaveTimeRequired}
data-leave-time={leaveTime.slice(0, -3)}
data-promotion-id={props.promotionId}
>
{friendlyTimeSlot(item.TimeSlot)}<br />{itemPrice}
</button>
);
});
this.setState({
times: time
});
}
clickHandler(e) {
e.preventDefault();
console.log(this.button.dataset);
}
render() {
if (this.state.times && this.props.name && this.props.description) {
return (
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">{this.props.name}</h3>
</div>
<div className="panel-body">
<p>{this.props.description}</p>
{this.state.times}
</div>
</div>
);
}
return (
<p>No times available.</p>
);
}
}
TimeSlotStack.propTypes = {
name: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired,
timeSlots: React.PropTypes.array.isRequired,
preferredTimeSlot: React.PropTypes.string.isRequired,
promotionId: React.PropTypes.number
};
export default TimeSlotStack;
When I then click a button, I always get the last button from each list. Hopefully the screenshot below will help make this clearer:
The log above comes from:
clickHandler(e) {
e.preventDefault();
console.log(this.button.dataset);
}
...but was generated by clicking the first buttons of each row. You can see that it always outputs the last only.
Is there something I'm doing wrong? This is my first React project and it's gotten me all flustered. Please let me know if I'm doing something that's not the React way that could be causing this.
Thanks!
You are overwriting the button variable, this in this context is a reference to a TimeSlotStack instance. To do what you want you need to maintain a list of buttons, for instance.
constructor() {
super();
this.clickHandler = this.clickHandler.bind(this);
this.buttons = [];
this.state = {
times: undefined
};
}
....
// using a IFE so `clickHandler` is called with the correct index
((idx) => {
time.push(
<button
className="btn btn-default"
type="button"
onClick={(e) => this.clickHandler(e, idx)}
ref={button => {
this.buttons.push(button);
}}
key={idx}
data-time={friendlyTime}
data-leave-time-required={leaveTimeRequired}
data-leave-time={leaveTime.slice(0, -3)}
data-promotion-id={props.promotionId}
>
{friendlyTimeSlot(item.TimeSlot)}<br />{itemPrice}
</button>
);
})(i);
....
clickHandler(e, i) {
e.preventDefault();
console.log(this.buttons[i].dataset);
}

Categories

Resources