react componentdidupdate provokes infinite loop - javascript

I trigger an API call to elasticsearch with onChange so that I can prompt a list for autocomplete.
To ensure that my store was updated before rerender I added componentDidMount so that I am not one tick behind. But getting to the point needs code, so:
constructor(props) {
super(props)
this.state = {
inputValue: '',
options: []
};
this.props = {
inputValue: '',
options: []
};
this._onChange = this._onChange.bind(this);
}
componentDidMount() {
CodeStore.addChangeListener(this._onChange);
}
_onChange(event) {
if (this.state.inputValue && !event) {
var enteredChars = this.state.inputValue;
console.log('NO EVENT!');
} else if (event.target.value) {
console.log('EVENT!');
var enteredChars = event.target.value;
this.setState({inputValue: enteredChars});
}
CodeService.nextCode(enteredChars);
}
As you can see I added some log events just to get sure my condition is doing right. I read about that setState provokes a rerender so it is inside the condition but that didn't had stopped the loop . And the console log confirms the condition switch. But having the setState inside my condition brakes the functionality and I do not get a list.
Here is my log:
0
Home.jsx:48 EVENT!
Home.jsx:50 0
CodeService.js:27 request
CodeActions.js:10 dispatch
CodeStore.js:22 store
Home.jsx:43 1
Home.jsx:46 NO EVENT!
10OptionTemplate.jsx:15 render-list
CodeService.js:27 request
CodeActions.js:10 dispatch
CodeStore.js:22 store
Home.jsx:43 1
Home.jsx:46 NO EVENT!
10OptionTemplate.jsx:15 render-list
CodeService.js:27 request
CodeActions.js:10 dispatch
CodeStore.js:22 store
Home.jsx:43 1
Home.jsx:46 NO EVENT!
The infinity loop does not hurt the performance anyhow. I think because of componentWillUnmount but the massive amount of API calls have to be avoided. Hope this is enough information for any evidence.

Looks like the infinite loop is caused by the following pattern:
user types input
_onChange updates state + fires action to store
Store updates itself and send change event
Store change event fires same _onChange event
_onChange does not update state, but fires action to store nonetheless
Goto step 3. and repeat (indefinitely)
Steps to take to fix:
make different change handlers for user input and store update (as already suggested by #Janaka Stevens)
put the data from store in state
user input does not have to be in state (if your react code does not provide a value to an input field, it will start with empty value, and leave whatever is typed alone)
Then your code could look something like this:
constructor(props) {
super(props)
this.state = {
options: []
};
// removed the this.props bit: props should not be updated inside component
this._onChangeUser = this._onChangeUser.bind(this);
this._onChangeStore = this._onChangeStore.bind(this);
}
componentDidMount() {
CodeStore.addChangeListener(this._onChange); // this is OK
}
_onChangeUser(event) {
console.log('EVENT!');
var enteredChars = event.target.value;
CodeService.nextCode(enteredChars);
// No need to update state here - go to sleep until store changes
}
_onChangeStore() {
var newList = getListFromStore(); // your own function to get list from store
this.setState({ options: newList}); // update state here
}
Not sure what you meant by 'one tick behind', or what could have caused it, but cleaning up endless loop is a good place to start ;)
PS: Code above is sketch only, may have typo's.

It looks like you are using _onChange for both your input event and store listener. They need separate methods.

Related

How do I update useState immidiatly for function React

I have created a function to calculate the winner between the two selected pokemon. However, instead of using the newly selected option, it is using the previously selected option. It has been brought to my attention that this is because useState is not updating immediately so how would I go about fixing this?
Here is my winner function:
function selectedWinner(){
console.log(pokemonName+' '+pokeOneTotal);
console.log(pokemonName2+' '+pokeTwoTotal);
if(pokeOneTotal>pokeTwoTotal){
setPokemonWinner(pokemonName);
}else if(pokeOneTotal<pokeTwoTotal){
setPokemonWinner(pokemonName2);
}else{
setPokemonWinner("Draw");
}
}
I have set it so that it is called in the different select functions, which are on click functions, here is one as an example:
function optionOneSelected(){
console.log('selected');
axios.get('https://pokeapi.co/api/v2/pokemon/'+ pokemonOne.current.value)
.then((res)=>{
let data=res.data;
console.log(data);
let type = data.types[0].type.name;
let id = data.id;
let height= data.height;
let weight = data.weight;
let name = data.forms[0].name;
let hp = data.stats[0].base_stat;
//console.log(type)
setPokemonType(type);
setPokemonId(id);
setPokemonHeight(height);
setPokemonWeight(weight);
setPokemonName(name);
setPokemonHp(hp);
let sum=0;
sum= data.stats[0].base_stat+ data.stats[1].base_stat+ data.stats[2].base_stat+ data.stats[3].base_stat+data.stats[4].base_stat+data.stats[5].base_stat;
setPokeOneTotal(sum);
let pokemonOneDataList = [
data.stats[0].base_stat, data.stats[1].base_stat, data.stats[2].base_stat, data.stats[3].base_stat,data.stats[4].base_stat,data.stats[5].base_stat
];
let labels = [
'hp', 'Attack', 'Defense', 'Special Attack', 'Special Defense', 'Speed'
];
setPokemonOneData(pokemonOneDataList);
setDataLabels(labels);
selectedWinner();
})
}
You can call useEffect with pokeOneTotal and pokeTwoTotal as dependencies. Whenever pokeOneTotal or pokeTwoTotal updates, it will trigger useEffect
useEffect(() => {
if(pokeOneTotal>pokeTwoTotal){
setPokemonWinner(pokemonName);
}else if(pokeOneTotal<pokeTwoTotal){
setPokemonWinner(pokemonName2);
}else{
setPokemonWinner("Draw");
}
}, [pokeOneTotal, pokeTwoTotal])
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, it will likely run before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
// Whatever we want to do after the state has been updated.
}, [state])
This console.log will run only after the state has finished changing and a render has occurred.
Note: "state" in the example is interchangeable with whatever state piece you're dealing with.
Check the documentation for more info.
Either:
Pass the new values to selectedWinner as arguments instead of reading from the state.
Move the call to selectedWinner into a separate useEffect hook that has those state variables as dependencies (so it gets called when, and only when, any of them change).

Wait for state update useState hook

I have an array of objects in state: const [objects, setObjects] = useState([]);
I have an add button that adds an object to the array, this is the body of the onclick event:
setObjects([...objects, {
id: 20,
property1: value1
}]);
I have a remove button that removes an object from the array, this is the body of the onclick event:
const newObjects = objects.filter(object => {
return object.id !== idToRemove; // idToRemove comes from the onclick event
});
setObjects(newObjects);
Now I want to do something with the updated state if an object gets removed from the state.
The problem is I have to wait till the state is updated and I don't want to listen for every state change, only if something is removed.
This is what I have so far:
useEffect(() => {
//execute a function that uses the updated state
}, [objects.length]);
But this also fires of if an object gets added to the state.
In short: I want to do something when an object gets removed from the objects array and the state is finished updating
I would like to do this with hooks.
Thanks in advance!
You could write your own hook or add some other variable that will keep the array's length and which could be used to check whether element was removed or added.
const [len, setLen] = useState(0);
useEffect(() => {
if (objects.length < len) {
// Your code
}
setLen(objects.length);
}, [objects.length]);

How to make sure a React state using useState() hook has been updated?

I had a class component named <BasicForm> that I used to build forms with. It handles validation and all the form state. It provides all the necessary functions (onChange, onSubmit, etc) to the inputs (rendered as children of BasicForm) via React context.
It works just as intended. The problem is that now that I'm converting it to use React Hooks, I'm having doubts when trying to replicate the following behavior that I did when it was a class:
class BasicForm extends React.Component {
...other code...
touchAllInputsValidateAndSubmit() {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let inputs = {};
for (let inputName in this.state.inputs) {
inputs = Object.assign(inputs, {[inputName]:{...this.state.inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in inputs) {
inputs[inputName].touched = true;
}
// UPDATE STATE AND CALL VALIDATION
this.setState({
inputs
}, () => this.validateAllFields()); // <---- SECOND CALLBACK ARGUMENT
}
... more code ...
}
When the user clicks the submit button, BasicForm should 'touch' all inputs and only then call validateAllFields(), because validation errors will only show if an input has been touched. So if the user hasn't touched any, BasicForm needs to make sure to 'touch' every input before calling the validateAllFields() function.
And when I was using classes, the way I did this, was by using the second callback argument on the setState() function as you can see from the code above. And that made sure that validateAllField() only got called after the state update (the one that touches all fields).
But when I try to use that second callback parameter with state hooks useState(), I get this error:
const [inputs, setInputs] = useState({});
... some other code ...
setInputs(auxInputs, () => console.log('Inputs updated!'));
Warning: State updates from the useState() and useReducer() Hooks
don't support the second callback argument. To execute a side effect
after rendering, declare it in the component body with useEffect().
So, according to the error message above, I'm trying to do this with the useEffect() hook. But this makes me a little bit confused, because as far as I know, useEffect() is not based on state updates, but in render execution. It executes after every render. And I know React can queue some state updates before re-rendering, so I feel like I don't have full control of exactly when my useEffect() hook will be executed as I did have when I was using classes and the setState() second callback argument.
What I got so far is (it seems to be working):
function BasicForm(props) {
const [inputs, setInputs] = useState({});
const [submitted, setSubmitted] = useState(false);
... other code ...
function touchAllInputsValidateAndSubmit() {
const shouldSubmit = true;
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
for (let inputName in inputs) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
// UPDATE STATE
setInputs(auxInputs);
setSubmitted(true);
}
// EFFECT HOOK TO CALL VALIDATE ALL WHEN SUBMITTED = 'TRUE'
useEffect(() => {
if (submitted) {
validateAllFields();
}
setSubmitted(false);
});
... some more code ...
}
I'm using the useEffect() hook to call the validateAllFields() function. And since useEffect() is executed on every render I needed a way to know when to call validateAllFields() since I don't want it on every render. Thus, I created the submitted state variable so I can know when I need that effect.
Is this a good solution? What other possible solutions you might think of? It just feels really weird.
Imagine that validateAllFields() is a function that CANNOT be called twice under no circunstances. How do I know that on the next render my submitted state will be already 'false' 100% sure?
Can I rely on React performing every queued state update before the next render? Is this guaranteed?
I encountered something like this recently (SO question here), and it seems like what you've come up with is a decent approach.
You can add an arg to useEffect() that should do what you want:
e.g.
useEffect(() => { ... }, [submitted])
to watch for changes in submitted.
Another approach could be to modify hooks to use a callback, something like:
import React, { useState, useCallback } from 'react';
const useStateful = initial => {
const [value, setValue] = useState(initial);
return {
value,
setValue
};
};
const useSetState = initialValue => {
const { value, setValue } = useStateful(initialValue);
return {
setState: useCallback(v => {
return setValue(oldValue => ({
...oldValue,
...(typeof v === 'function' ? v(oldValue) : v)
}));
}, []),
state: value
};
};
In this way you can emulate the behavior of the 'classic' setState().
I have tried to solve it using the useEffect() hook but it didn't quite solve my problem. It kind of worked, but I ended up finding it a little too complicated for a simple task like that and I also wasn't feeling sure enough about how many times my function was being executed, and if it was being executed after the state change of not.
The docs on useEffect() mention some use cases for the effect hook and none of them are the use that I was trying to do.
useEffect API reference
Using the effect hook
I got rid of the useEffect() hook completely and made use of the functional form of the setState((prevState) => {...}) function that assures that you'll get a current version of your state when you use it like that. So the code sequence became the following:
// ==========================================================================
// FUNCTION TO HANDLE ON SUBMIT
// ==========================================================================
function onSubmit(event){
event.preventDefault();
touchAllInputsValidateAndSubmit();
return;
}
// ==========================================================================
// FUNCTION TO TOUCH ALL INPUTS WHEN BEGIN SUBMITING
// ==========================================================================
function touchAllInputsValidateAndSubmit() {
let auxInputs = {};
const shouldSubmit = true;
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
return({
...auxInputs
});
});
validateAllFields(shouldSubmit);
}
// ==========================================================================
// FUNCTION TO VALIDATE ALL INPUT FIELDS
// ==========================================================================
function validateAllFields(shouldSubmit = false) {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs =
Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// ... all the validation code goes here
return auxInputs; // RETURNS THE UPDATED STATE
}); // END OF SETINPUTS
if (shouldSubmit) {
checkValidationAndSubmit();
}
}
See from the validationAllFields() declaration that I'm performing all my code for that function inside a call of setInputs( (prevState) => {...}) and that makes sure that I'll be working on an updated current version of my inputs state, i.e: I'm sure that all inputs have been touched by the touchAllInputsValidateAndSubmit() because I'm inside the setInputs() with the functional argument form.
// ==========================================================================
// FUNCTION TO CHECK VALIDATION BEFORE CALLING SUBMITACTION
// ==========================================================================
function checkValidationAndSubmit() {
let valid = true;
// THIS IS JUST TO MAKE SURE IT GETS THE MOST RECENT STATE VERSION
setInputs((prevState) => {
for (let inputName in prevState) {
if (inputs[inputName].valid === false) {
valid = false;
}
}
if (valid) {
props.submitAction(prevState);
}
return prevState;
});
}
See that I use that same pattern of the setState() with functional argument call inside the checkValidationAndSubmit() function. In there, I also need to make sure that I'm get the current, validated state before I can submit.
This is working without issues so far.

setState delay in react

I currently have a function in react native which does the following:
resetThenSet = (id, arrayId, title) => {
if(arrayId != 'selectProduct') {
// Setting state for each selected dropdown in selectedDropdowns
this.setState({
dataShowingToggle: true,
selectedDropdowns: {...this.state.selectedDropdowns, [arrayId]: title}
}, this.getProductCost(arrayId, id)
);
}
I run the above and I can confirm arrayId and title variables are valid and contain data. arrayId is also not 'selectProduct'. I added a console.log in there while debugging to ensure it runs, which it indeed does. The expected behavior I would expect is that the state is updated immediately.
However the selectedDropdowns isn't updated in state. When I add:
console.log(this.state) after the this.setState update there is no change. If I run the function twice it'll update on the second time it runs.
To test it even further I added static inputs like so:
this.setState({
dataShowingToggle: true,
selectedDropdowns: {...this.state.selectedDropdowns, testField: 'me here'}
}, this.getProductCost(arrayId, id)
);
console.log(this.state);
It only update state AFTER the first time it runs. Am I missing something?
UPDATE:
I updated the code to run the console.log on the call back to setstate:
if(arrayId != 'selectProduct') {
// Setting state for each selected dropdown in selectedDropdowns
console.log(arrayId + title);
console.log('i run');
this.setState({
dataShowingToggle: true,
selectedDropdowns: {...this.state.selectedDropdowns, [arrayId]: title}
}, console.log(this.state)
);
};
console.log is 'quantityProduct 25' for the arrayId + title
console.log(I run)
and then this state is NOT showing the quantityProduct 25
setState is asynchronous. This is why the function call has an optional callback.
I think you might have other issues.
Try putting that console log inside the callback function when you call setState to see when the state is updated with the values you gave it.

How to wait for a child component task to be completed before proceed

this is my first question here, sorry if it's not well structured.
I have a parent component, component1, that has a form,
and it calls component2, which also has a form.
Component2 has its own database collection, but it only can be saved if parent component were too. So it has an Input property called write, that component1 set to true when user click on save button.
This Input is been checked with OnChanges, by component2.
NgOnChanges call writeOnDB(), and this is working fine.
writeOnDB try to persists the component2 form data, and emit an Output saved, with true on success, or false on fail.
So, component2 is being called like this:
<component2
[write]="saveForm"
(saved)="onCmp2Saved($event)">
</component2>
My submit function is like this:
onSubmit(form) {
// Get form values and save into component1 object
this.cmp1 = form;
// Set Flag to save component2 on BD
this.saveForm = true;
let result = this._cmp1Service.editData(this.cmp1)
//TODO: Check if component2 successfully saved its form before proceed
result.subscribe(
res => _router.navigate['home'],
error => ...
);
}
So the question is,
how do I wait for the component2 emit saved output?
I always find my answers here, but I think I'm not knowing how to search this, cause I didn't find anything.
Thanks!
It looks like you need to wait for two things to happen before you continue:
a saved event from component2
an event from editData()
Since you don't know which will finish first, you'll need each handler to check to see if the other completed. Something like:
onSubmit(form) {
this.cmp1 = form;
this.saveForm = true;
this.component2Done = false;
this.component1Done = false;
this._cmp1Service.editData(this.cmp1).subscribe(
res => {
this.component1Done = true;
if(this.component2Done) { doSomething(); }
},
error => ...
);
}
onCmp2Saved(result) {
this.component2Done = true;
if(this.component1Done) { doSomething(); }
}

Categories

Resources