Reactjs, is setState needed here, changing value of an object in state - reactjs

My question is, is it ok to not use setState in this situation or if I should, how would I go about it?
I'm creating a sportsbetting app.
On the list of games, when a user clicks on Denver Broncos -3, a bet slip is created. This also calls
setState({gameSelected:[item]}) in the Parent Component.
It stores that object and adds to it using the spread operator when more bets are clicked.
I pass down that data to betSlip.js file.
side note
Some of the data in the gameSelected object has :
risk:'',
win:'',
On betSlip.js I am looping through that data and displaying it.
Relevant code inside the loop.
<input type="text" placeholder="Risk" onChange={(e)=>this.handleBet(e,item)}/>
<input type="text" placeholder="Win" value={item.win}/>
This function is inside betSlip.js
handleBet = (e,item) =>{
let bet=e.target.value
//function to calculate moneyline bet
let calcBet =(bet)=>{
let newAmount =(100/item.juice)*bet;
return newAmount
}
item.win = calcBet(bet)
}

Yes, you should be using setState here to update item.win. Otherwise, React is not aware of the mutation on item and does not rerender. Since item is a prop, you should treat it as immutable from within betSlip.js and update item.win through an event handler in the parent component where item is maintained. Your betSlip.js component could have an onWinUpdate handler that is called within handleBet as this.props.onWinUpdate(item, calcBet(bet)). Then, the parent component can have a function handleWinUpdate(item, bet) that updates the state. I would also keep things immutable in handleWinUpdate by doing something like this:
handleWinUpdate(item, bet) {
this.setState(state => ({
gameSelected: state.gameSelected.map(i => {
if (i === item) {
return { ...i, win: bet };
} else {
return i;
}
})
}));
}
Notice that we are setting gameSelected to a new array and replacing item in that array with a new copy of item, but with a new win value. No objects are ever mutated.
Even though you don't have to use immutability here, I find it easier to reason about and it opens the door for performance gains later using things like React.PureComponent that rely on immutability.

Related

useEffect not triggering when object property in dependence array

I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}

Why does Object.keys(this.refs) not return all keys?

Hi,
so I've redacted some sensitive information from the screen shot, but you can see enough to see my problem.
Now, I'm trying to build the UI for a site that gets data from a weather station.
I'm trying to use react-google-maps' InfoBox, which disables mouse events by default.
It seems that to enable mouse events, you must wait until the DOM is loaded, and then add the event handlers.
react-google-maps' InfoBox fires an onDomReady event (perhaps even upon adding more divs) but seems to never fire an onContentChanged event (I've looked in the node_modules code).
The content I'm putting in the InfoBox is basically a div with a string ref for each type of weather data. Sometimes there comes along a new type of weather data so I want to put that in also, and have the ref be available / usable.
However, immediately after the new divs have been added (and the DOM has been updated to show them), when I try to console log the DOM nodes (the refs refer to the nodes because they are divs and not a custom built component) the latest added ones are undefined.
They do become a div (not undefined) a few renders later.
I've contemplated that this may be because
1) the DOM is not being updated before I'm trying to access the refs, but indeed the UI shows the new divs,
2) string refs are deprecated (React 16.5),
but they work for the divs in comonentDidMount and eventually for new divs in componentDidUpdate,
3) executing the code within the return value of render may be run asynchronously with componentDidMount, but I also tried setTimeout with 3000 ms to the same effect,
4) of something to do with enumerable properties, but getOwnProperties behaves the same way.
In the end I decided I'll console log this.refs and Object.keys(this.refs) within the same few lines of code (shown in the screen shot), and you can see that within one console log statement (where Object.keys was used in the previous line) that while this.refs is an object with 8 keys, the two most recently added refs don't appear in Object.keys(this.refs).
This is probably a super complex interaction between react-google-maps' InfoBox, React's refs, and JavaScript's Object.keys, but it seems like it should be simple and confuses me to a loss.
Can anyone shed some light on why this might be happening??
The code looks something alike:
class SensorInfoWindow extends React.Component {
handleIconClick = () => {
// do stuff here
}
componentDidMount() {
this.addClickHandlers();
}
componentDidUpdate() {
this.addClickHandlers();
}
addClickHandlers = () => {
const keys = Object.keys(this.refs);
for(let i=0; i<keys.length; i++) {
const key = keys[i];
let element = this.refs[key];
if (element !== undefined)
element.addEventListener('click', this.handleIconClick);
}
}
render() {
const { thissensor, allsensors } = this.props;
let divsToAddHandlersTo = [];
const sensorkeys = Object.keys(allsensors);
for (let i=0; i<sensorkeys.length; i++) {
divsToAddHandlersTo.push(
<div
ref={'stringref' + i}
/>
{/* children here, using InfoBox */}
</div>
);
}
return (
<div>
{divsToAddHandlersTo}
</div>
);
}
}
This is, in essence, the component.

Is it ok to modify part of prevState inside setState?

Is there a real disadvantage to modifying part of prevState and returning that part inside setState() ?
Example:
this.setState(prevState => {
prevState.myObject.isItTrue = !prevState.myObject.isItTrue;
return {myObject: prevState.myObject};
});
Rather than:
this.setState(prevState => {
const myObject = Object.assign({}, prevState.myObject);
myObject.isItTrue = !myObject.isItTrue;
return {myObject: myObject};
});
Is there any real disadvantage to the first code where I save myself the Object.assign() ?
EDIT: If I am correct, prevState.myObject is simply a reference to this.state.myObject, so changing prevState.myObject actually changes this.myObject.object as well! However, this doesn't seem to break anything, as long as I use setState() to pass an object that contains the new data, even if it's just a reference to the old objects inside this.state.
Do you agree that this is still ok, i.e. it won't break anything to do it like this?
Following documentation:
state is a reference to the component state at the time the change is being applied. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from state and props.
https://reactjs.org/docs/react-component.html
So you should not apply changes directly to that state.
Either way, why not do something like this?:
this.setState(prevState => ({
myObject : {
...prevState.myObject,
isItTrue: !prevState.myObject.isItTrue,
}
}));
This way will get all the elements from the prevState but also change all the ones you want to modify.
First prevState and the this.state are the same object. So you are modifying the actual state directly.
Secondly, down the road you might pass the myObject as a prop to another component, and since it will be the same object always that component will not know that something has changed and it will not re-render (for example PureComponent and ones that implement componentDidUpdate or shouldComponentUpdate and test for changes)
See https://codesandbox.io/s/zen-aryabhata-m07l4 for showcases of all issues.
So you should use
this.setState(state => ({
myObject: {
...state.myObject,
isItTrue: !state.myObject.isItTrue
}
}));

Modify Child state from parent just once

I have created a <Form> Component that makes all the form-fields you give to it controlled by injecting value and onChange props to the form-fields by iterating through them. This has been working perfectly well for me for most of the forms I have created.
When I needed to have the functionality of the form-field values controlling some aspect of the parent state, I added a onFormValueChange prop to the Form that would get called whenever a field value gets updated. Using this I can track a subset of the changes to the Form's state.
However, now my problem is this...how do I override the value of a form-field, conditional on some event that occurs in the parent. I have not been able to figure out how to override the Form's state just once. If I give it a prop that sets an override value like {name: string, value: any}, then on every update this override value will override the form-field's value which is not good.
These are the solutions I thought of but they seem extremely hacky and I was hoping someone in the SO community can help.
Set an override prop on the Form Component which times out after around 100ms and hope that the user doesn't try to modify the form in that tiny duration. But I dislike using setTimeout for hacks like these.
Pass a disableOverride function along with the overrideValue prop. Then in my Form's shouldComponentUpdate I can just call disableOverride() in the callback of the setState I will use to override the value. Something like:
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.override) {
const { name, value } = nextProps.override;
const newState = Object.assign({}, this.state, { [name]: value });
this.setState(newState, () => {
nextProps.disableOverride();
});
return false;
}
return true;
}
But this also feels unnecessarily complicated, and possibly quite vulnerable to crashing unexpectedly.
EDIT Some further clarification: The point of this would be so that for example if I have 'country' and 'city' fields then if 'country' is cleared, then the 'city' should get cleared too. I can track the state of 'country' with onFormValueChange but don't have an API to modify the Form state in order to clear the 'city' field.
I came up with a solution. It was absurdly simple, I dont know why it took me so long.
componentDidUpdate(prevProps, prevState) {
if (!isEqual(prevProps.override, this.props.override)) {
const { name, value } = nextProps.override;
this.setState({
[name]: value
});
}
}
isEqual is an object comparison function taken from lodash

Why is using refs slowing down my React application?

I need a few refs of elements to be stored in Redux so that the elements can be focused on.
I have this dropdown:
<BasicSelect
selectRef={(e) => this.storeRef('make', e) } ... />
And here is storeRef:
storeRef(list, ref)
{
if(this.state.refsStored[list]) {
return;
} else {
this.props.storeSelectRef(list, ref);
var refState = Object.assign({}, this.state.refsStored);
refState[list] = true;
this.setState({refsStored: refState});
}
}
From this, it should store the ref once, then simply return after comparing.
However, every time a option in the dropdown of <BasicSelect> is clicked on, the application hangs for a <1s (noticable), and then continues on.
If I change storeRef to the following (obviously the intended result doesn't work):
storeRef(list, ref) {
return;
}
The dropdown selection is super fast, and all is good. So how come this comparison if(this.state.refsStored[list]) is significantly slow?
Don't put refs into component state. Component state should only be used for things that you will need when rendering, and should cause a re-render when you update them. Save refs directly onto the component instance:
<BasicSelect selectRef={selectInstance => this.selectInstance = selectInstance} />
I'm also kind of confused what selectRef is as a prop, to be honest, but I assume that the <BasicSelect> component forwards that ref on to an underlying <select>tag?
Anyway, the key point is that you are causing unnecessary re-renders every time because you're calling setState(). Don't do that.

Resources