I get an error when calling this.props.fetch() in the mySort method. this.props.mySort collects new data from the backend sorted by the respective column. The error is: Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
this.props.fetch is now called in an infinite loop.
How can I fix this?
Best regards,
Joachim
constructor(props) {
super(props)
let columns = [
{ field: "foo", header: "bar", sortable: true, sortFunction: this.mySort }
]
this.state = {
cols: columns
}
this.colOptions = []
for (let col of this.state.cols) {
this.colOptions.push({ label: ` ${col.header}`, value: col })
}
}
mySort = (e) => {
if (e.order === 1) {
console.log("1")
this.props.fetch({...)}
} else {
...}
}
render() {
let columnData = this.state.cols.map((col, i) => {
return <Column className="columnheader" style={col.style} key={col.field} field={col.field}
header={col.header} sortable={col.sortable} sortFunction={col.sortFunction} body={col.body} expander={col.expander} />
})
return(
<DataTable value={fetchResults} >
{columnData}
</DataTable>
)
}
You shouldn't be doing a fetch from inside a render, and the way it is, seems it's being called when every single column is rendered causing multiple state updates resulting in an infinite loop
Instead, move the fetch to componentDidMount. And whenever the sort field is changed, handle the change event and sort the data client side and reset your data, so that the whole table is already sorted and can be rendered at once. If the data is huge and needs the server to sort it for whatever reasons, again handle the refetch in the change event, and set the entire sorted data into state at one go.
Something like,
componentDidMount(){
// fetch the data
}
handleSortFieldChange(){
/* sort client side and -> setState()
or fetch again and setState()*/
}
I changed the sortFunction this way:
mySort = (e) => {
if (e.order === 1 && e.field !== this.state.sortField) {
this.setState({
sortField: e.field
},
this.props.fetchSearchResults({
...
}))
} else if (e.order === -1 && e.field !== this.state.sortField) {
this.setState({
sortField: e.field
},
this.props.fetchSearchResults({
...
}))
}
}
Now, there is no infinite loop but the error stays the same.
Related
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.
**I'm trying to create an array with 5 values which I could use with nameArray[number].
I think that the declaration of the array is wrong but I don't know how I can fix it.
My idea is that: I have 5 buttons, when I click one of this, only one value of the 5 values in the state array change from false to true.
**
constructor(props) {
super(props);
this.state = {
activeButtons: [false, false, false, false, false]
};
}
cliccato = (e) => {
e.preventDefault();
const number = parseInt(e.target.id);
this.setState(
{
activeButtons: !this.state.activeButtons[number],
},
() => {
console.log(" "+ this.state.activeButtons[number]);
}
);
}
You're updating your state's activeButtons with a single boolean value, rather than an updated array.
You need to generate a new array and modify only the relevant element:
const newArray = [...this.state.activeButtons];
newArray[number] = !newArray[number];
this.setState({
activeButtons: newArray,
});
Declaration of the array is fine. You can make it shorter with Array(5).fill(false).
It's setting state part that needs work. In your current code, you are setting the state to the alternate of a boolean value, instead you need to set it to an array.
this.setState(prevState => ({
activeButtons: prevState.activeButtons.map((val, index) => {
if(index === number) {
return !val;
}
return val;
})
}));
Also, using the functional set state form here
It's because you're overwriting activeButtons every time with only one element value. You need to preserve the other values each time you want to update an element.
Using an Object would be a more graceful data structure in this case though.
this.setState(
{
activeButtons: {
...this.state.activeButtons
[number]: !this.state.activeButtons[number]
}
)
I am having an array of objects which is a state object that corresponds to a user account. Just for a validation purpose if any one of the object property within array is empty, I am setting a validation property of state object to false. Since I am looping using for loop, the setState is not setting data.
this.state = {
accounts: [{firstname:"",lastname:"",age:""}],
validated: true
};
onAdd = () => {
let { accounts,validated } = this.state;
for(let i=0; i< accounts.length; i++){
if(accounts[i].firstname === '' || accounts[i].age === '')
{
this.setState({validated: false},() => {}); // value is not getting set
break;
}
}
if(validated)
{
// some other operation
this.setState({validated: true},() => {});
}
}
render(){
let { validated } = this.state;
return(
{validated ? <span>Please fill in the details</span> : null}
//based on the validated flag displaying the error message
)
}
Try to have a separate check that goes through the array in the state and depending if the data is missing or not you can have the temporary variable be either true or false. Then based on the temp variable you can set the state accordingly.
Let me know if this works:
onAdd = () => {
let { accounts,validated } = this.state;
let tempValidate = true; //assume validate is true
for(let i=0; i< accounts.length; i++){
if(accounts[i].firstname === '' || accounts[i].age === '')
{
//this loop will check for the data validation,
//if it fails then you set the tempValidate to false and move on
tempValidate = false
break;
}
}
// Set the state accordingly
if(tempValidate)
{
this.setState({validated: true},() => {});
} else {
this.setState({validated: false},() => {});
}
}
There are multiple issues in your code. First of all this.setState is asynchronous and therefore you can not say when it really is executed.
So if you call
this.setState({validated: false},() => {});
you can not rely that after that this.state.validated is false immediatly.
So after you call this.setState to make validated == false and then check:
if(this.state.validated)
{
// some other operation
this.setState({validated: true},() => {});
}
It might be either true or false.
But in your code you're extracting validated (what you're using in the if-condition) at the beginning of your method. That means validated won't change when this.state changes.
You might want to use the callback you already added (() => {}) to perform some other action after the state changed or just use a normal valiable instead of something related to the state.
Like:
tmp = true;
for-loop {
if(should not validate) {
tmp = false;
this.setState({validated: false});
break;
}
if(tmp) {
this.setState({validated: true},() => {});
}
setState is async function. So it won't work in loops.
From the docs
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.
Also refer this.
I'm having trouble accessing data inside my state which I fetch from my database,
this is my state:
state = {
inputDataArr: [],
inputDataArrTest: [
{
formID: 'test',
inputArr: [1, 2, 3],
labelArr: [4, 5, 6]
}
]
};
this is the collection I'm importing from the database:
{
"_id": {
"$oid": "5ba96b8ebffd923734090df4"
},
"inputArr": [
"value1",
"value2",
"value3"
],
"labelArr": [
"label1",
"label2",
"label3"
],
"formID": "5ba96b83bffd923734090df0",
"__v": 0
}
which is an object with 2 arrays in it,
this is how I fetch it :
componentWillMount() {
fetch('/api/datas')
.then(data => data.json())
.then(datas => {
const filterdDatas = datas.filter(
data => data.formID == this.props.match.params.id
);
this.setState(currentState => ({
inputDataArr: [...currentState.inputDataArr, ...filterdDatas]
}));
});
}
now when I console log inputDataArr and inputDataArrTest I get an array with an object inside,
exactly the same,
but when I console log InputDataArrTest[0] which is what you can see in my state I can see the arrays and the formID,
but when I console log inputDataArr[0] I get undefined, really frustrating, anybody knows why?
Mapping and doing data manipulation inside the render is never a good idea and isn't easy to debug.
My suggestion is to initialize the state to an empty array, and call a method to return the mapped data.
constructor(props) {
super(props);
this.state = {
inputDataArr: []
};
}
render() {
const data = this.state.inputDataArr.map(...)
return <div>{data}</div>
}
This way state is initiated before first render and data will return an empty array.
After componentDidMount the state will update and will re-render with the new data.
As a side note, i would do the mapping in a third then and set the state to the result itself.
Happy Coding.
I'm not sure where are you running the console.log but setState has a callback method that you can run after the actual data has been updated:
this.setState(currentState => ({
inputDataArr: [...currentState.inputDataArr, ...filterdDatas]
}), () => {
// this is the callback for setState, you can access the updated data
console.log(inputDataArr);
});
Note If you run the console.log in render keep in mind that the first render call will always run before the async data has been fetched.
Note #2 Do not use componentWillMount at all and especially not for fetching data.
Use componentDidMount instead.
Have you test filteredDatas before using ...filteredDatas ? Maybe it is not an Array and result to other thing or an empty Array.
Are you sure this.props.match.params.id is defined and equal to the formID property?
If it exists only one object with unique formID why don’t you use Array#find instead of Array.filter and then update your state with
this.setState(prevState => ({
dataArr: filteredDatas ? prevState.dataArr.concat([filteredDatas]) : prevState.dataArr // Here with Array#find, filteredDatas is null or a object not an Array
});
renderLabels() {
const { inputDataArr } = this.state;
return (
!!inputDataArr.length &&
inputDataArr[0].labelArr.map((label, index) => (
<th key={index}>{label}</th>
))
);
}
conditional rendering solved my problem
I'm rendering a list of products according to a specific value. I'm doing the render with a Picker Component and when it's different of 306, I'm loading the selected products but IF I come back on the first PickerItem (306) I want to show ALL the products again...
For instance :
if (Value != '306') {
this.setState({
// isAll: false,
labels,
Colours: Value,
filteredProducts: this.state.displayProducts.filter(product => product.colour == Value)
}, () => {
this.onLoadFilteredLabels();
});
} else {
this.setState({
// isAll: true,
Colours: Value,
displayProducts: this.state.products,
displayLabels: this.state.labels
});
}
I'm looking for some advice if there is a better way of doing this ?
Do you think I should separe every setState ?
It's working but I have the feeling that it's a bit tricky and I'm still learning. So I know I can have goods advices here with people here !
Thank you
The easiest approach is to create a custom object for setting states and just passing arguments in a custom method. Apart from that, using ternary could be beneficial:
let stateObj = value === '306' ? {obj : {
// isAll: true,
Colours: Value,
displayProducts: this.state.products,
displayLabels: this.state.labels
}, func : () => {
return false;
}} : {obj : {
// isAll: false,
labels,
Colours: Value,
filteredProducts: this.state.displayProducts.filter(product => product.colour == Value)
}, func : () => {
this.onLoadFilteredLabels();
}}
this.stateSetter(stateObj);
Next define your custom method:
stateSetter = (stateObj) => {
let {obj, func} = stateObj;
this.setState(obj,func);
}
You can make use of ternary operator, equality check etc. So that your code will look clean. For example,
this.setState({
isAll: Value === '306',
filterProducts: Value === '306' ? [yourFilterProducts] : []
});
Generally, it's ok if you have several setState() calls in different branches of a condition.
But in your case, it's better to update in the state only Colours and filter products directly inside the render method:
render() {
const {products: allProducts, Colours} = this.state;
const displayProducts = Value != '306' ? allProducts.filter(product => product.colour == Colours) : allProducts;
return (
<div>
{displayProducts.map(product => <YourProductComponent key={product.id} product={product}/>)}
<div>
);
}
React documentation recommends that if you can calculate required data from state or/and props you should do it instead of introducing new prop or state variable.
PS: there's a common recommendation to not to refer to this.state when in setState().
React runs setState in batch so you can end up with referring to outdated state. You should pass a function as the first argument of setState. More details here.