I'm trying to learn ReactJS and found out about these lifecycles. However I've a doubt regarding how componentDidUpdate() functions and I want to know the reason behind it. Please take a look at the code below. It is a simple code that calculates the area of a triangle. Just to understand how we can send data from parent to child and vice versa I'm initialising the variables in parent component, then passing them to child to calculate the area and then passing back the results to change the state to final one where the results are rendered.
App.js
class App extends React.Component{
constructor(props){
super(props);
console.log("In the parent's constructor");
let initializeState=[{'base' : 0,'height' : 0,'area' : 0}];
this.state={ values : initializeState }
console.log(this.state.values);
}
componentDidMount(){
console.log("Mounting values");
let b = [{}];
b['base']=10;
b['height']=20;
b['area']=0;
this.setState({values: b});
}
update = (base,height,area) => {
console.log("Updating state with calculated area");
let updatedValue = [{}];
updatedValue['base'] = base;
updatedValue['height'] = height;
updatedValue['area'] = area;
this.setState({values: updatedValue});
}
render(){
console.log('Inside Parent render');
return(<React.Fragment>
<Triangle val = {this.state.values} update={this.update} />
</React.Fragment>
)
}
}
class Triangle extends React.Component{
shouldComponentUpdate(newProps, newState){
console.log('validating data');
if(newProps.val.base >0 && newProps.val.height >0){
return true;
}else{
return false;
}
}
componentDidUpdate(prevProps, prevState, snapshot){
console.log('In the child componentDidUpdate');
console.log('If you\'ve reached this, state has been re-rendered')
console.log(prevProps.val.base);
console.log(prevProps.val.height);
console.log(prevProps.val.area);
}
calcArea = () => {
console.log('Calculating area now');
let area = 1/2* this.props.val.base * this.props.val.height;
this.props.update(this.props.val.base,this.props.val.height,area);
}
render(){
console.log("In the child's render method")
return(
<React.Fragment>
<h2>Base : {this.props.val.base}</h2>
<h2>Height : {this.props.val.height}</h2>
<h2>Area : {this.props.val.area} </h2>
<button onClick={this.calcArea}>Click to calculate AREA</button>
</React.Fragment>
)
}
}
export default App;
So Everything is working fine, i.e., This is the series of output:
In the parent's constructor
App.js:11 [{…}]
App.js:33 Inside Parent render
App.js:66 In the child's render method
App.js:15 Mounting values
App.js:33 Inside Parent render
App.js:43 validating data
App.js:66 In the child's render method
App.js:52 In the child componentDidUpdate
App.js:53 If you've reached this, state has been re-rendered
Now till this point, the component has re-rendered according to the new values mentioned inside componentDidMount function. However the next output is :
App.js:54 undefined
App.js:55 undefined
App.js:56 undefined
It should be the values that have been destroyed. ie., 0, 0, 0 (mentioned inside the parent's constructor using this.state ). Though when I hit the calculate Area and the component is re-rendered, it shows the correct values which have been destroyed. ie.,
Calculating area now
App.js:24 Updating state with calculated area
App.js:33 Inside Parent render
App.js:43 validating data
App.js:66 In the child's render method
App.js:52 In the child componentDidUpdate
App.js:53 If you've reached this, state has been re-rendered
App.js:54 10
App.js:55 20
App.js:56 0
Can someone tell me why the results are "undefined" when the state is being changed for the first time and not the values that we have instantiated ??
There are some issues in the JavaScript which are probably the reason why you are getting undefined values.
let initializeState=[{'base' : 0,'height' : 0,'area' : 0}];
this.state={ values : initializeState }
Here you are setting the state as an object with a key values which then holds an array of objects [{}].
This leads to issues in other places where an array of objects is defined but then it is accessed like an object:
let b = [{}];
b['base']=10;
This code should be:
let b = {};
b.base = 10;
There is no need to use bracket notation when you are using a "normal" string as the key. This notation is used when using variables for the key or string keys that start with a number, have hyphens, etc:
const a_string = 'base';
const a_string_with_hyphen = 'some-key-name';
an_object[a_string] = 123;
an_object[a_string_with_hyphen] = 456;
Use let when defining a variable which value will change, otherwise use a const:
let value_that_will_change = 123;
const value_that_wont_change = 456;
value_that_will_change = 789;
Regarding react specifically, I changed the code a little to show different approaches, so that you can see how the state is changing. I used inputs to modify the values, that I think can be handy in this case:
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = { base: 0, height: 0, area: 0 };
console.log("App. constructor.", "state:", this.state);
}
componentDidMount() {
const values = {
base: 10,
height: 20,
area: 200
};
this.setState(values);
console.log(
"App. componentDidMount.",
"state:",
this.state,
"Updating state with new values:",
values
);
}
componentDidUpdate(prevProps, prevState) {
console.log(
"App. componentDidUpdate.",
"prev state:",
prevState,
"new state:",
this.state
);
}
updateBase = base_new => {
this.setState({
base: base_new,
area: (1 / 2) * base_new * this.state.height
});
console.log("App. updateBase.", "new base:", base_new);
};
updateHeight = event => {
const height_new = parseInt(event.target.value);
this.setState({
height: height_new,
area: (1 / 2) * height_new * this.state.base
});
console.log("App. updateHeight.", "new height:", height_new);
};
doubleBase = () => {
const { base, height } = this.state;
const base_new = 2 * base;
const area_new = (1 / 2) * base_new * height;
this.setState({ base: base_new, area: area_new });
console.log("App. doubleBase.", "new area:", area_new);
};
render() {
const { state, updateBase, updateHeight, doubleBase } = this;
console.log("App. render.", "state:", this.state);
return (
<Triangle
{...state}
updateBase={updateBase}
updateHeight={updateHeight}
doubleBase={doubleBase}
/>
);
}
}
class Triangle extends React.Component {
componentDidMount() {
const {
updateBase,
updateHeight,
doubleBase,
...parent_state_props
} = this.props;
console.log("Triangle. componentDidMount.", "props:", parent_state_props);
}
componentDidUpdate(prevProps) {
const {
updateBase,
updateHeight,
doubleBase,
...parent_state_props
} = this.props;
const {
updateBase: updateBase_prev,
updateHeight: updateHeight_prev,
doubleBase: doubleBase_prev,
...parent_state_props_prev
} = prevProps;
console.log(
"Triangle. componentDidUpdate.",
"prev props:",
parent_state_props_prev,
"new props:",
parent_state_props
);
}
render() {
const {
updateBase,
updateHeight,
doubleBase,
...parent_state_props
} = this.props;
const { base, height, area } = parent_state_props;
console.log("Triangle. render.", "props:", parent_state_props);
return (
<React.Fragment>
<label for="base" style={{ display: "block" }}>
Base
</label>
<input
id="base"
type="number"
value={base}
// Here we are defining the function directly and sending the value.
onChange={event => updateBase(parseInt(event.target.value))}
/>
<label for="height" style={{ display: "block" }}>
Height
</label>
<input
id="height"
type="number"
value={height}
// Here we are sending the event to the function.
onChange={updateHeight}
/>
<h2>{`Area: ${area}`}</h2>
<button onClick={doubleBase}>Double base</button>
</React.Fragment>
);
}
}
In the above code I left shouldComponentUpdate out. This method is used to prevent the Component from rendering. The reason for this is that every time a parent Component renders, it will make all its children render. This is ok if the props that the children receives changed, but not necessary when those received props didn't change. Basically nothing changed in the children but it is still rendering. If this supposes a performance issue you can use the PureComponent instead of Component, or use your own logic in shouldComponentUpdate.
One last thing, if you accept the advice, I encourage you to learn React Hooks which were introduced this year and is the new way to build with React.
Let me know if something is not clear or I missed something.
On componentDidMount in App component, we are setting the new values ,
previously they were undefined.
Now, inside componentDidUpdate in Triangle component, you are logging the prevProps which were never there, as a result they are undefined.
componentDidUpdate(prevProps, prevState, snapshot){
console.log('In the child componentDidUpdate');
console.log('If you\'ve reached this, state has been re-rendered')
console.log(prevProps.val.base);
console.log(prevProps.val.height);
console.log(prevProps.val.area);
console.log(this.props.val.base);
console.log(this.props.val.height);
console.log(this.props.val.area);
}
Change as above, you will get to know that the new props are set.
Related
I am trying to build a chat application with the functionality of input field which can be used as filter for chat_groups array which is in the state as chat_groups. Here is how my code looks:
constructor(props) {
super(props);
this.state = {
currUserId: "--id--",
chats: [],
chat_groups: [],
users: [],
};
}
.
.
.
<input
className="chat-group__search__input"
placeholder="Search for group..."
onChange={(ev) => {
console.log(ev.currentTarget.value);
var thatState = this.state;
thatState.chat_groups = thatState.chat_groups.map(
(gp) => {
gp["visible"] = gp.group_name
.toLowerCase()
.includes(ev.currentTarget.value);
return gp;
}
);
// getting correct state in thatState variable
this.setState(thatState);
}}
/>
// getting old state in setState callback and componentDidUpdate lifecycle
The weird problem is I am getting the correct value in thatState variable before setting state. But after setState function is called, if I try to check the state in setState callback or componentDidUpdate lifecycle, I am getting the old state only.
I tried that for keydown and change events also. So, seems to be less of an issue of event as well.
I would like to know if some issue in the code is evident or there is something that I can do to debug the issue.
Edit: After changes, my current onChange looks as below, but the issue is still there; the setState function does not seem to change the state as I can see only the old state in componentDidUpdate lifecycle and setState callback.
onChange={(ev) => {
console.log(ev.currentTarget.value);
let chat_groups = this.state.chat_groups.map((gp) => ({
...gp,
visible: gp.group_name
.toLowerCase()
.includes(ev.currentTarget.value),
}));
console.log(
"Before",
chat_groups.map((gp) => gp.visible)
);
this.setState({ chat_groups: chat_groups });
}}
The problem is that you are mutating the state.
When you do var thatState = this.state; the reference is still the same for both the objects. So automatically when you update thatState.chat_groups you are updating/mutating state as well.
Change your onChange method to like below
onChange = ev => {
console.log(ev.currentTarget.value);
let { chat_groups } = this.state;
chat_groups = chat_groups.map(gp => ({
...gp,
visible: gp.group_name.toLowerCase().includes(ev.currentTarget.value)
}));
this.setState(state => ({
...state,
chat_groups
}));
};
//Other code
//....
//....
<input
className="chat-group__search__input"
placeholder="Search for group..."
onChange={this.onChange} />
I suspect there's one problem while checking the group_name with the input value i.e., you are converting the group_name to lower case using gp.group_name.toLowerCase() but the input value you are not converting to lower case. This could be one issue why the visible attribute value is not getting updated. So in the below snippet I have converted the input value also to lower case while comparing.
Here, below is a runnable snippet with your requirement. Doing console.log in the setState's callback function and the state is getting updated.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
currUserId: "--id--",
chats: [],
chat_groups: [{
group_name: "Group One"
}, {
group_name: "Group Two"
}],
users: []
}
}
onChange = ev => {
console.log(ev.currentTarget.value);
let {
chat_groups
} = this.state;
chat_groups = chat_groups.map(gp => ({
...gp,
visible: gp.group_name.toLowerCase().includes(ev.currentTarget.value.toLowerCase())
}));
this.setState(state => ({
...state,
chat_groups
}), () => { console.log(this.state.chat_groups); });
};
render() {
return <input
placeholder="Search for group..."
onChange={this.onChange} />
}
}
ReactDOM.render(<App />, document.getElementById("react"));
.as-console-wrapper {
max-height: 100% !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>
No, don't do this var thatState = this.state it's just an object it will easily get mutate and you will not get the update as for react state change never occured.
instead do this var { chat_groups } = this.state and then use it further and inlast set the state this.setState({ chat_groups: chat_groups }) also try to avoid the mutation as much as possible means copy the values of chat_groups also
Seems like you are trying to manipulate state directly which is a big no in React.
onChange={(ev) => {
this.setState({
chat_groups: this.state.chat_groups.map((gp) => {
gp["visible"] = gp.group_name
.toLowerCase()
.includes(ev.currentTarget.value);
return gp;
})
});
}}
State should only be updated using the setState method.
You are mutating the state directly in your code above - this isn't recommended. You would get unexpected results and it's not predictable.
This is how you should do it - create a new updated object and pass it to the setState.
onChange={(ev) => {
console.log(ev.currentTarget.value);
const updatedChatGroups = this.state.chat_groups.map((gp) => {
const visible = gp.group_name.toLowerCase().includes(ev.currentTarget.value);
return {
...gp,
visible,
};
});
// Update the modified object using this.setState().
this.setState({ chat_groups: updatedChatGroups });
}}
Read More
So I have an array is a class's state. Let's call it A. A is populated with objects of type B through a function f in the constructor. Later, I generate using f and new data, a new array of objects of type B called C. I then use setState({A: C}). However, this results in the data from the first array staying on the display. I'm not sure how to fix this at all.
Edit: code snippets
class ClassBox extends Component {
constructor(props) {
super(props);
// note data here uses the keys from the config file
this.state = {
data: this.props.data,
filteredData: [],
functionData: []
};
this.generateFunctionData = this.generateFunctionData.bind(this);
this.state.functionData = this.generateFunctionData();
this.state.filteredData = this.state.functionData;
this.handleSearch = this.handleSearch.bind(this);
}
generateFunctionData(useData = false, data = null){
return useData ? ProcessJSON.extractFunctions(data.functions).map((_function, index) =>
{return createMethodBox(_function.Func_name, _function.Description, _function.Access_Mod, index)}) : ProcessJSON.extractFunctions(this.props.data.functions).map((_function, index) =>
{return createMethodBox(_function.Func_name, _function.Description, _function.Access_Mod, index)});
}
handleSearch(input) {
// convert to lower case to avoid capitalization issues
const inputLowerCase = input.toString().toLowerCase();
// filter the list of files based on the input
const matchingList = this.state.functionData.filter((method) => {
return method.props.name.toLowerCase().includes(inputLowerCase)
}
);
this.setState({
filteredData: matchingList
});
}
render() {
console.log(this.state.filteredData)
return (
<Container>
<NameContainer>
<h1>{this.state.data.className}</h1>
</NameContainer>
<ContentContainer>
<DescriptionContainer>
{this.state.data.description}
</DescriptionContainer>
<StyledDivider/>
<VarContainer>
<h1>Variables</h1>
<VarTableContainer>
<BootstrapTable
keyField="id"
data={[]}
columns={testColumns}
bordered={false}
noDataIndication="Table is Empty"
classes="var-table"
/>
</VarTableContainer>
{/*{this.state.data.variables}*/}
</VarContainer>
<StyledDivider/>
<MethodContainer>
<MethodHeader>
<h1>Methods</h1>
<StyledSearchBar onSearch={this.handleSearch}
isDynamic={true} allowEmptySearch={false} minChars={0}
className='searchBar'/>
</MethodHeader>
<Methods>
{this.state.filteredData}
</Methods>
</MethodContainer>
</ContentContainer>
</Container>
);
}
class Classes extends Component {
constructor(props) {
super(props);
this.state = {
data: this.props.data,
displayIndex: this.props.displayIndex
};
this.treeData = createTreeData(this.state.data);
this.classBox = null
}
componentDidUpdate(prevProps, prevState, snapshot) {
if(prevState.displayIndex !== this.props.displayIndex){
const funcData = this.classBox.generateFunctionData(true, this.state.data[0][this.props.displayIndex]);
console.log(funcData);
this.classBox.setState({data: this.state.data[0][this.props.displayIndex], functionData: funcData, filteredData: funcData });
this.classBox.forceUpdate();
this.setState({displayIndex: this.props.displayIndex});
}
}
render() {
this.treeData = createTreeData(this.state.data);
return (
<Container>
<FileTreeContainer>
<StyledTreeMenu data={treeData}/>
</FileTreeContainer>
<ClassInfoContainer>
<ClassBox ref = {el => this.classBox = el} data = {this.state.data[0][this.state.displayIndex]}/>
</ClassInfoContainer>
</Container>
)
}
Classes contains an instance of ClassBox. After executing componentDidUpdate, the page still shows the old method boxes, even though functionData has changed.
EDIT 2: it's also worth noting that when I replace the class view with the landing view and go back to the class view it shows the page correctly.
The way your are setting the state should be correct, as you are setting it to a newly created array from .filter.
I think the issue is with you storing the method components in the filteredData state. Components should not be stored in state.
I believe your component is just re-rendering, but not removing the old generated components. Maybe try mapping the search input to the state and generate the method components that way.
It feels very weird, but I am creating a new React component in my app in a fully analogous way to how I did it before with the other components and it is just not updating at all. The components' lifecycle methods are not called at all.
I have topmost ControlledTabs component which renders SingleDatasetDashboard which looks the following way:
class SingleDatasetDashboard extends React.Component {
constructor(props){
super(props);
}
render() {
return (
<div>
{
this.props.page === this.props.pageNames[1] ?
<div>
<MarkerGenesChart mainData={this.props.mainData} clusterIds={this.props.clusterIds} page = {this.props.page}
allGenesData={this.props.allGenesData} datasetName = {this.props.datasetName} pageNames = {this.props.pageNames}/>
</div>
: ""
}
{
this.props.page === this.props.pageNames[2] ?
<div>
<ClustersMultiChart mainData = {this.props.mainData} datasetName = {this.props.datasetName}
page = {this.props.page} pageNames = {this.props.pageNames}/>
</div>
: ""
}
<div style={{visibility: this.props.page === this.props.pageNames[0] ? 'visible' : 'hidden' }}>
<ScatterChart key="main" datum = {this.props.mainData} type = "main" datasetName = {this.props.datasetName}
page = {this.props.page} pageNames = {this.props.pageNames}/>
</div>
</div>)
}
}
module.exports = SingleDatasetDashboard;
Here MarkerGenesChart and its child ScatterChart render perfectly well. Another ScatterChart at the very bottom is rendering fine too. Inside ClustersMultiChart I am having the folowing code:
import React from 'react';
import Col from 'react-bootstrap/lib/Col';
import Row from 'react-bootstrap/lib/Row';
import ScatterChart from './ScatterChart';
class ClustersMultiChart extends React.Component {
constructor(props){
super(props);
}
render() {
return (
this.props.mainData.map((clusterObj, idx) => {
return <Col xs={4} md={4} key={"cluster" + idx + "chart"} >
Hello! I am No {idx}
<ScatterChart datum = {this.props.mainData}
type = "cluster" datasetName = {this.props.datasetName}
page = {this.props.page} pageNames = {this.props.pageNames}/>
</Col>
})
)
}
}
module.exports = ClustersMultiChart;
There is also a child ScatterChart as in MarkerGenesChart. However, here the issue comes: ScatterChart only produces single svg tag and it is never updated at all. Its render() has:
<Col xs={5} md={5}>
<svg ref={node => this.node = node} width={1000} height={1000}></svg>
</Col>
Any suggestions would be greatly appreciated.
Update
If I remove { this.props.page === this.props.pageNames[2] ? from SingleDatasetDashboard, it renders perfectly. However, I need to leave it there somehow, and it is working for MarkerGenesChart for some reason. I checked whether the page is switched correctly and it is.
Update
This is definitely not related to ScatterChart component. Deleting it completely from ClustersMultiChart does not fix the absence of updates in it. Interesting thing is that render() is called but nothing else. Main data object datum is also passed since I am able to iterate over it.
Update
Calling setState() in the topmost component after switching the page triggers the lifecycle methods of the ClustersMutltiChart. Here is how I am triggering the change of the page and the timeout function fixes the issue by calling the setState() after some time:
showNextPage = (pageNum) => {
const pageIdx = pageNum - 1;
const page = pageNames[pageIdx];
this.setState({
page: page
});
setTimeout(function() { //Start the timer
this.setState({
datasetName: this.state.datasetName
})
}.bind(this), 3000);
}
Obviously I want it to happen without such forced update after some time. Just calling this.forceUpdate() without setTimeout does not fix the issue.
Finally I got it to work in an acceptable for me way. Just calling this.forceUpdate() in componentDidMount of ClustersMutltiChart made it to work:
ClustersMutltiChart.jsx:
componentDidMount() {
this.forceUpdate();
}
I am trying to build a react grid component that has design similar to the one shown in image.
(1) Someone passes raw data to grid as props, (2) which gets converted to multiple GridData objects and stored within GRID. (3) Those objects are iterated over in render function to render grid items. (4) Now, someone performs an action outside of grid (may be in toolbar or something), that triggers property change for some/all GridData objects stored within Grid (in this case select all). (5) This results in all grid items getting updated property (in this case all items will be selected).
However, when I update the attribute of an object in Grid, the child (GridItem) does not check the checkbox. How can I fix that?
The code for the design looks something like this:
Grid.js
class Grid extends Component {
constructor(props) {
super(props);
this.state = {
gridData: props.data,
};
}
componentWillReceiveProps(nextProps) {
this.setState({
gridData:
typeof nextProps.gridData !== 'undefined' && nextProps.gridData
? nextProps.gridData
: this.state.gridData,
});
}
// PubSub System to receive notification
subscriber(msg, data) {
if(msg === 'SELECT_ALL_ITEMS'){
this.state.gridData.forEach(gridItem => {
gridItem.setChecked(true);
}
}
renderGridItem(gridItem) {
return (
<GridItem
key={gridItem.getItemId()}
title={gridItem.getTitle()}
isChecked={gridItem.isChecked()}
/>
);
}
render() {
return (
<div>
{this.state.gridData !== 'undefined' && this.state.gridData ? (
this.state.gridData.map(gridItem => this.renderGridItem(gridItem))
) : (
<div />
)}
</div>
);
}
}
GridItem.js
class GridItem extends Component {
constructor(props) {
super(props);
this.state = {
isChecked: typeof props.isChecked !== 'undefined' && props.isChecked ? props.isChecked : false,
title: typeof props.title !== 'undefined' && props.title ? props.title : '',
},
};
}
render() {
const { classes } = this.props;
return (
<div>
<Checkbox
checked={this.state.isChecked}
/>
{this.state.properties.title}
</div>
);
}
}
GridData.js
export default class GridData {
constructor(item) {
this._title = item.title;
this._itemId = item.itemId;
}
getItemId() {
return this._entryId;
}
isChecked() {
return this._isChecked;
}
setChecked(isChecked) {
this._isChecked = isChecked;
}
getTitle() {
return this._title;
}
}
I think you need to put your subscriber into componentDidMount
This method is a good place to set up any subscriptions. If you do that, don’t forget to unsubscribe in componentWillUnmount()
And you have to update state with setState in your subscriber.
Calling setState() in componentDidMount method will trigger an extra rendering
I'm writing a script which moves dropdown below or above input depending on height of dropdown and position of the input on the screen. Also I want to set modifier to dropdown according to its direction.
But using setState inside of the componentDidUpdate creates an infinite loop(which is obvious)
I've found a solution in using getDOMNode and setting classname to dropdown directly, but i feel that there should be a better solution using React tools. Can anybody help me?
Here is a part of working code with getDOMNode (i
a little bit neglected positioning logic to simplify code)
let SearchDropdown = React.createClass({
componentDidUpdate(params) {
let el = this.getDOMNode();
el.classList.remove('dropDown-top');
if(needToMoveOnTop(el)) {
el.top = newTopValue;
el.right = newRightValue;
el.classList.add('dropDown-top');
}
},
render() {
let dataFeed = this.props.dataFeed;
return (
<DropDown >
{dataFeed.map((data, i) => {
return (<DropDownRow key={response.symbol} data={data}/>);
})}
</DropDown>
);
}
});
and here is code with setstate (which creates an infinite loop)
let SearchDropdown = React.createClass({
getInitialState() {
return {
top: false
};
},
componentDidUpdate(params) {
let el = this.getDOMNode();
if (this.state.top) {
this.setState({top: false});
}
if(needToMoveOnTop(el)) {
el.top = newTopValue;
el.right = newRightValue;
if (!this.state.top) {
this.setState({top: true});
}
}
},
render() {
let dataFeed = this.props.dataFeed;
let class = cx({'dropDown-top' : this.state.top});
return (
<DropDown className={class} >
{dataFeed.map((data, i) => {
return (<DropDownRow key={response.symbol} data={data}/>);
})}
</DropDown>
);
}
});
You can use setStateinside componentDidUpdate. The problem is that somehow you are creating an infinite loop because there's no break condition.
Based on the fact that you need values that are provided by the browser once the component is rendered, I think your approach about using componentDidUpdate is correct, it just needs better handling of the condition that triggers the setState.
The componentDidUpdate signature is void::componentDidUpdate(previousProps, previousState). With this you will be able to test which props/state are dirty and call setState accordingly.
Example:
componentDidUpdate(previousProps, previousState) {
if (previousProps.data !== this.props.data) {
this.setState({/*....*/})
}
}
If you use setState inside componentDidUpdate it updates the component, resulting in a call to componentDidUpdate which subsequently calls setState again resulting in the infinite loop. You should conditionally call setState and ensure that the condition violating the call occurs eventually e.g:
componentDidUpdate: function() {
if (condition) {
this.setState({..})
} else {
//do something else
}
}
In case you are only updating the component by sending props to it(it is not being updated by setState, except for the case inside componentDidUpdate), you can call setState inside componentWillReceiveProps instead of componentDidUpdate.
This example will help you to understand the React Life Cycle Hooks.
You can setState in getDerivedStateFromProps method i.e. static and trigger the method after props change in componentDidUpdate.
In componentDidUpdate you will get 3rd param which returns from getSnapshotBeforeUpdate.
You can check this codesandbox link
// Child component
class Child extends React.Component {
// First thing called when component loaded
constructor(props) {
console.log("constructor");
super(props);
this.state = {
value: this.props.value,
color: "green"
};
}
// static method
// dont have access of 'this'
// return object will update the state
static getDerivedStateFromProps(props, state) {
console.log("getDerivedStateFromProps");
return {
value: props.value,
color: props.value % 2 === 0 ? "green" : "red"
};
}
// skip render if return false
shouldComponentUpdate(nextProps, nextState) {
console.log("shouldComponentUpdate");
// return nextState.color !== this.state.color;
return true;
}
// In between before real DOM updates (pre-commit)
// has access of 'this'
// return object will be captured in componentDidUpdate
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log("getSnapshotBeforeUpdate");
return { oldValue: prevState.value };
}
// Calls after component updated
// has access of previous state and props with snapshot
// Can call methods here
// setState inside this will cause infinite loop
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("componentDidUpdate: ", prevProps, prevState, snapshot);
}
static getDerivedStateFromError(error) {
console.log("getDerivedStateFromError");
return { hasError: true };
}
componentDidCatch(error, info) {
console.log("componentDidCatch: ", error, info);
}
// After component mount
// Good place to start AJAX call and initial state
componentDidMount() {
console.log("componentDidMount");
this.makeAjaxCall();
}
makeAjaxCall() {
console.log("makeAjaxCall");
}
onClick() {
console.log("state: ", this.state);
}
render() {
return (
<div style={{ border: "1px solid red", padding: "0px 10px 10px 10px" }}>
<p style={{ color: this.state.color }}>Color: {this.state.color}</p>
<button onClick={() => this.onClick()}>{this.props.value}</button>
</div>
);
}
}
// Parent component
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { value: 1 };
this.tick = () => {
this.setState({
date: new Date(),
value: this.state.value + 1
});
};
}
componentDidMount() {
setTimeout(this.tick, 2000);
}
render() {
return (
<div style={{ border: "1px solid blue", padding: "0px 10px 10px 10px" }}>
<p>Parent</p>
<Child value={this.state.value} />
</div>
);
}
}
function App() {
return (
<React.Fragment>
<Parent />
</React.Fragment>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
I would say that you need to check if the state already has the same value you are trying to set. If it's the same, there is no point to set state again for the same value.
Make sure to set your state like this:
let top = newValue /*true or false*/
if(top !== this.state.top){
this.setState({top});
}
this.setState creates an infinite loop when used in ComponentDidUpdate when there is no break condition in the loop.
You can use redux to set a variable true in the if statement and then in the condition set the variable false then it will work.
Something like this.
if(this.props.route.params.resetFields){
this.props.route.params.resetFields = false;
this.setState({broadcastMembersCount: 0,isLinkAttached: false,attachedAffiliatedLink:false,affilatedText: 'add your affiliate link'});
this.resetSelectedContactAndGroups();
this.hideNext = false;
this.initialValue_1 = 140;
this.initialValue_2 = 140;
this.height = 20
}
I faced similar issue. Please make componentDidUpdate an arrow function. That should work.
componentDidUpdate = (params) => {
let el = this.getDOMNode();
if (this.state.top) {
this.setState({top: false});
}
if(needToMoveOnTop(el)) {
el.top = newTopValue;
el.right = newRightValue;
if (!this.state.top) {
this.setState({top: true});
}
}
}
I had a similar problem where i have to center the toolTip. React setState in componentDidUpdate did put me in infinite loop, i tried condition it worked. But i found using in ref callback gave me simpler and clean solution, if you use inline function for ref callback you will face the null problem for every component update. So use function reference in ref callback and set the state there, which will initiate the re-render
You can use setState inside componentDidUpdate