Component did update behaving abnormally - reactjs

I have implemented a block where i'm using componentDidUpdate to call a function if 2 of my condition meets the certain criteria. When both the condition satisfies it is calling the function which is then going to an infinite loop and executing the function for infinite times.
I have 2 drop-downs which are there to select the values. If both of them are having values, then call the function for this I'm using componentDidUpdate to keep an eye on changes on both the state variables.
When drop-down value change it will set the state to state variable which i'm using in the condition.
Below is my code:
handleRegionChange(idx: any, event: any) {
const region= [""];
region[idx] = event.label;
this.setState({ region});
}
handleProductChange(idx: any, event: any) {
const productId= [""];
productId[idx] = event.key;
this.setState({ productId});
}
componentDidUpdate() {
if (
this.state.regionId?.[0] && this.state.productId?.[0]) {
this.props.fetchValues( // Redux action function which accepts 2 parameters
this.state.regionId?.[0],
this.state.productId?.[0]
);
}}
Please help me in highlighting the issue or through some light on how to tackle or use component did update in this kind of situation.
Thanks.

It's causing an infinite loop mostly likely because fetchValues will update your component props from it's parent, this triggers another update, which will run componentDidUpdate again.
One easy way to fix this is to prevent further update, if the id has not been changed for any dropdown values.
componentDidUpdate(prevProps, prevState) {
const hasRegionIdChanged = this.state.regionId !== prevState.regionId;
const hasProductIdChanged = this.state.productId !== prevState.productId;
if (hasRegionIdChanged || hasProductIdChanged ) {
this.props.fetchValues(
this.state.regionId?.[0],
this.state.productId?.[0]
);
}
}
https://reactjs.org/docs/react-component.html#componentdidupdate
Further reading, see how React introduced the hook pattern to let you think of these things beforehand, requiring a dependencies list:
https://reactjs.org/docs/hooks-reference.html#useeffect
2nd attempt:
// Not sure if this is a multi select dropdown,
// It should be either [] or a plain value
// Multi select dropdown
handleRegionChange(idx: any, event: any) {
// You should not need to know which index is was, all you need is the region label ... (Updated for remove of duplication)
// If you have access to the spread operator, then JSON.stringify is not required below
const region = [...new Set([...this.state.region, event.label])];
this.setState({ region});
}
// Single select dropdown
handleProductChange(idx: any, event: any) {
this.setState({ productId: event.key});
}
componentDidUpdate(prevProps, prevState) {
// If it's an array/object, you need to compare them deeply
const hasRegionIdChanged = JSON.stringify(this.state.regionId) !== JSON.stringify(prevState.regionId);
const hasProductIdChanged = this.state.productId !== prevState.productId;
if (hasRegionIdChanged || hasProductIdChanged ) {
if (this.state.regionId?.length && this.state.productId) {
this.props.fetchValues(
this.state.regionId,
this.state.productId
);
}
}
}

Related

Two independent state piece causing infinite loop - useEffect

I can't wrap my head around the problem I'm experiencing Basically I submit the form and it checks whether or not there are empty values. I then paint the input border either red or green. However, I need to repaint the border all the time, meaning that if user enters a value, the border should turn green (hence the useEffect). I have 2 pieces of state here. One keeps track of validation error indexes (for value === '') The other piece is the createForm state (form fields) itself.
I then send down the indexes via props.
NOTE: infinite loop occurs not on initial render, but when form is submitted with empty values. The infinite loop DOES NOT occur if there is no empty field on form submit.
I'm willing to share additional info on demand.
const [createForm, setCreateForm] = React.useState(() => createFormFields);
const [validationErrorIndexes, setValidationErrorIndexes] = React.useState([]);
//Function that is being triggered in useEffect - to recalculate validaiton error indexes and resets the indexes.
const validateFormFields = () => {
const newIndexes = [];
createForm.forEach((field, i) => {
if (!field.value) {
newIndexes.push(i);
}
})
setValidationErrorIndexes(newIndexes);
}
//(infinite loop occurs here).
React.useEffect(() => {
if (validationErrorIndexes.length) {
validateFormFields();
return;
}
}, [Object.values(createForm)]);
//Function form submit.
const handleCreateSubmit = (e) => {
e.preventDefault();
if (createForm.every(formField => Boolean(formField.value))) {
console.log(createForm)
// TODO: dispatch -> POST/createUser...
} else {
validateFormFields();
}
}
//I then pass down validationErrorIndexes via props and add error and success classes conditionally to paint the border.
{createForm && createForm.length && createForm.map((formEl, i) => {
if (formEl.type === 'select') {
return (
<Select
className={`create-select ${(validationErrorIndexes.length && validationErrorIndexes.includes(i)) && 'error'}`}
styles={customStyles}
placeholder={formEl.label}
key={i}
value={formEl.value}
onChange={(selectedOption) => handleOptionChange(selectedOption, i)}
options={formEl.options}
/>
)
}
return (
<CustomInput key={i} {...{ label: formEl.label, type: formEl.type, value: formEl.value, formState: createForm, formStateSetter: setCreateForm, i, validationErrorIndexes }} />
)
})}
Ok, so here is what is happening:
Initial render - validationErrorIndexes is empty, bugged useEffect does not hit if and passes.
You click submit with 1 empty field - submit calls validateFormFields, it calculates, setValidationErrorIndexes is set, now its length is non zero and if in bugged useEffect will be hit. And here we got a problem...
The problem: on each rerender of your component Object.values(createForm) which are in your dependency array are evaluated and returning new [array]. Every single rerender, not the old one with same data, the new one with same data. And asuming the if guard is gone now due to length is non 0 - validateFormFields is called again. Which does its evaluations, and setting new data with setValidationErrorIndexes. Which causes rerendering. And Object.values(createForm) returns a new array again. So welp.
So basically, 2 solutions.
One is obvious and just replace [Object.values(createForm)] with just [createForm]. (why do you need Object.values here btw?)
Second one - well if it a must to have [Object.values(createForm)].
const validateFormFields = () => {
const newIndexes = [];
console.log(createForm);
createForm.forEach((field, i) => {
if (!field.value) {
newIndexes.push(i);
}
});
console.log(newIndexes);
setValidationErrorIndexes((oldIndexes) => {
// sorry, just an idea. Compare old and new one and return old if equals.
if (JSON.stringify(oldIndexes) === JSON.stringify(newIndexes)) {
return oldIndexes; // will not cause rerender.
}
return newIndexes;
});
};

React setState componentDidUpdate React limits the number of .. when trying to update the state based on updated state

i have react program that need the data from other components. When the parent components send path data, an array will get all of the files name inside folder, and also i need to count the data length to make sure file list will not empty. Several data need to stored to state and will re-used for the next process. Below is the programs.
constructor() {
super()
this.state = {
fulldir: null,
listfileArray: [],
isDataExist: true
}
}
componentDidUpdate()
{
let path = this.props.retrievefilespath
let dirLocation = Utils.fulldirFunc(path)
let rdir = fs.readdirSync(dirLocation)
let countList = 0
rdir.forEach((filename) => {
this.state.listfileArray.push({
id: countList,
name: filename,
selected: false
})
countList++
})
if(countList > 0)
this.setState({
isDataExist: true
})
}
But this program returns error
Error Maximum update depth exceeded. This can happen when a component
repeatedly calls setState inside componentWillUpdate or
componentDidUpdate. React limits the number of nested updates to
prevent infinite loops.
Previously i used vuejs and use data to handle the data flow
data () {
return {
listfile: [],
isActive: false,
isDataExist: false,
arrayIdx: []
}
},
watch: {
lfdirHandler: function () {
//reset data from previous action
Object.assign(this.$data, getInitialData())
//electron filesystem
const fs = require('fs')
if(this.lfdirHandler.length > 0)
{
let dirLocation = Utils.fulldirFunc(this.lfdirHandler)
let rdir = fs.readdirSync(dirLocation)
var countList = 0
rdir.forEach((filename) => {
this.listfile.push({
id: countList,
name: filename,
selected: false
})
countList++
})
if(countList > 0)
this.isDataExist = true
}
},
So, what's the best solution to this problem? I mean, how to handle the data flow like what i've done with vue? is it right to use react state? Thanks.
i'm confuse with the documentation written here
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
after several test, i think the documentation should be written like following code:
componentDidUpdate(props) {
// Typical usage (don't forget to compare props):
// props.userID as previous data
if (this.props.userID !== props.userID) {
this.fetchData(this.props.userID);
}
}
this prevProps is confusing me.
So, the solution based on the documentation is, you need to compare, whether data inside props.userID is equal to this.props.userID.
Notes:
this.props.userID bring new data
props.userID bring old data
props.userID is saving your data from your previous action. So, if previously you setstate userID with foo. The program will save it to props.userID.
componentDidUpdate(props) {
// this.props.userID -> foo
// props.userID -> foo
if (this.props.userID !== props.userID) {
// false
// nothing will run inside here
}
}
componentDidUpdate(props) {
// this.props.userID -> foo
// props.userID -> bar
if (this.props.userID !== props.userID) {
// true
// do something
}
}
my program here:
componentDidUpdate()
{
let path = this.props.retrievefilespath
//....
}
is infinitely compare this.props.retrievefilespath with nothing. That's why the program return error.

State value not updating in react

I'm working on my first React application and I'm not understanding why the State doesn't have the updated value.
Here is my code:
const SlideOutPanel = forwardRef((props: any, ref: any) => {
const initCss: string = 'is-slide-out-panel';
const [compClass, setCompClass] = useState(initCss);
useImperativeHandle(ref, () => ({
open() {
open();
},
close() {
close();
},
}));
function refresh(): any {
let classVal: string = compClass;
if (props.direction === 'left-to-right') {
classVal = `${classVal} left-to-right`;
} else if (props.direction === 'right-to-left') {
classVal = `${classVal} right-to-left`;
}
if (Types().boolVal(props.userOverlay)) {
classVal = `${classVal} use-overlay`;
}
if (Types().boolVal(props.pushMain)) {
classVal = `${classVal} push-effect`;
}
if (props.theme === 'dark') {
classVal = `${classVal} theme-dark`;
}
setCompClass(classVal);
let classValdd: string = compClass;
}
function open(): void {
let classVal: string = compClass;
}
useEffect(() => {
refresh();
}, []);
return (
<section id={id} className={compClass}>
<div className="content">{props.children}</div>
</section>
);
});
I call refresh() when the components first load, which basically sets the className based on the passed props. At the end of the function, I set state "setCompClass" the value of "classVal" which works as I verified in Chrome Debugger. But on the same function I have the following line "let classValdd: string = compClass;" just to check what the value of "compClass" is and its always "is-slide-out-panel".
At first I thought it has to do with a delay. So when I call open() to do the same check, the value is still "is-slide-out-panel". So I'm a bit confused. Am I not able to read the state value "compClass"? Or am I misunderstanding its usage?
Setting the state in React acts like an async function.
Meaning that when you set it, it most likely won't finish updating until the next line of code runs.
So doing this will not work -
setCompClass(classVal);
let classValdd: string = compClass;
You will likely still end up with the previous value of the state.
I'm not exactly sure what specifically you're trying to do here with the classValdd variable at the end of the function block, but with function components in React, if we want to act upon a change in a state piece, we can use the built-in useEffect hook.
It should look like this -
useEffect(() => {
// Stuff we want to do whenever compClass gets updated.
}, [compClass]);
As you can see, useEffect receives 2 parameters.
The first is a callback function, the second is a dependency array.
The callback function will run whenever there is a change in the value of any of the members in that array.

setState not maintaining set value after value set with Callback

I'm working on an application where I multiple some values and use the sum on a computation a bit further. I quickly realized that this.setState wasnt working and I saw a callback being said to use a callback so I used this.setState({ estAlumTxt: estLBCarbonAl }, () => { however something strange is still happening. If I display the estAlumTxt variable in the console it's correct but if I display it lower down, it no longer has it's value set. This is my code.
calWasteCarbon = (event) => {
if (`${this.state.reAlumDrop}` === '1') {
const estLBCarbonAl = (1 * -89.38);
//this.setState({ estAlumTxt: estLBCarbonAl }
this.setState({ estAlumTxt: estLBCarbonAl }, () => {
console.log(this.state.estAlumTxt) // Returns the correct value set
}) } else {
this.setState({ estAlumTxt: 0 })
}
if (`${this.state.recPlasDrop}` === '1') {
const estLBCarbonPlas = (1 * -35.56);
this.setState({ estPlasTxt: estLBCarbonPlas })
} else {
this.setState({ estPlasTxt: 0 })
}
alert(this.state.estAlumTxt); //Alerted value is wrong.
Even if console.log reads out estAlumTxt correctly, the alert has it wrong. Gr
Calls to this.setState are asynchronous, meaning that they do not occur synchronously at the time they are called, but may occur at a later time.
This means, that while your function is executing, state.estAlumTxt hasn't been updated yet when you call the alert function.
Let's refactor your function a bit so that we only need to call setState once.
First lets extract those number consts out of the function, and I would advise putting them outside the component. There is no need to recalculate those consts every time the function is called.
Then lets create a stateChange variable where we can add the properties that we want to conditionally change. After we check the conditions, lets call setState, and alert the value of the stateChange.estAlumTxt
const estLBCarbonAl = 1 * -89.38;
const estLBCarbonPlas = 1 * -35.56;
calWasteCarbon = (event) => {
const stateChange = {};
if (`${this.state.reAlumDrop}` === '1') {
stateChange.estAlumTxt = estLBCarbonAl;
} else {
stateChange.estAlumTxt = 0;
}
if (`${this.state.recPlasDrop}` === '1') {
stateChange.estPlasTxt = estLBCarbonPlas;
} else {
stateChange.estPlasTxt = 0;
}
this.setState(stateChange);
alert(stateChange.estAlumTxt);
};

React + TypeScript - how to update state with single object with a "dynamic" set of changes

I want to update the state at the end of a list of conditional updates and accumulates the changes to be committed at once (to avoid async issues with setState).
interface IMyComponentState {
a: string;
b: string;
c: string;
}
(...)
updateState = (condition1: boolean, condition2: boolean) => {
const stateChanges: Partial<IMyComponentState> = {};
if (condition1) {
stateChanges.a = 'someValue 1';
}
if (condition2) {
stateChanges.b = 'someValue 2';
}
this.setState(
{ ...this.state, ...stateChanges },
() => this.doSomethingThatDependsOnState()
);
}
This works fine, but is there a way to do it without using this.state, like below?
this.setState(
{...stateChanges},
(...)
);
Here tslint is complaining that setState expects an object of type Pick<IMyComponentState, "a" | "b" | "c"> but that would require me to specify (and predict) the properties to be changed in advance, which is not possible. I heard that React's diffing algorithm checks the references, but I'm still concerned that putting the whole state object in setState would add an extra overhead or is unnecessary.
First of all, you don't need to spread this.state, React will only apply state changes to the specified keys.
Secondly, the type for setState intentionally doesn't use Partial<T>, this is because setting undefined on a key explicitly will perform a state update, so it uses Pick (GitHub issue here talking more about it)
To get around this issue, you can cast you state update to Pick<IMyComponentState, keyof IMyComponentState>;
updateState = (condition1: boolean, condition2: boolean) => {
const stateChanges = {};
if (condition1) {
stateChanges.a = 'someValue 1';
}
if (condition2) {
stateChanges.b = 'someValue 2';
}
this.setState(
{ ...stateChanges } as Pick<IMyComponentState, keyof IMyComponentState>,
() => this.doSomethingThatDependsOnState()
);
}

Resources