React.JS - ComponentDidUpdate Method Does Gives Infinite Loop for Axios Calls - javascript

Good day,
I'm trying to fetch a new data once a new props has changed on my route.
Here's my sample routing:
<Route path={'/students/:selectedPage'} exact component={Students} />
Basically, I have a method that fetches my data. So I'm invoking it in my componentDidMount() Here are my sample codes:
componentDidMount(): void {
this.getData()
}
getData(selectedPage){
axios.get(`http://localhost:1343/students/${selectedPage}`).then(response=>{
this.setState({
students: response.data
})
}).catch(error=>console.log(error)
}
For my componentDidUpdate method which trigger if there's new changes in the param of my url.
componentDidUpdate(): void {
if (this.props.match.params.selectedPage !== prevProps.selectedPage){
this.getData(this.props.match.params.selectedPage)
}
}
Unfortunately, it causes to have infinite request from the web api. Which makes the table laggy. I tried to create a state with a name, hasLoaded but it gives me an error message that says, infinite loop has detected.
Any advice?

Compare your Props in ComponentWillReceiveProps like this,
shouldComponentUpdate: function(nextProps, nextState) {
return nextProps.selectedPage != this.props.selectedPage ;
}
componentWillReceiveProps(nextProps) {
if (nextProps.selectedPage !== this.props.selectedPage ) {
//this.setState({ selectedPage : nextProps.selectedPage })
this.getData(nextProps.selectedPage)
}
}
or Do this instead Keep your selectedPage in state
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.selectedPage !== prevState.selectedPage ) {
return { selectedPage : nextProps.selectedPage };
}
else return null;
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.selectedPage !== this.props.selectedPage ) {
//Perform some operation here
//this.setState({ selectedPage : this.props.selectedPage });
this.getData(this.props.selectedPage)
}
}
```

Related

How to update state of component in componentDidUpdate() without being stuck in an infinite re render?

I have a component with a componentDidMount() method that calls a method called getData() which gets the initial data and sets the initial state of the component.
class LogsSettings extends React.Component {
constructor(props) {
super(props);
this.settingsUrls = [
"/ui/settings/logging"
];
this.state = {
configSettings: {},
formSchema: formSchema
};
this.configSettings = {};
this.selected = "general";
}
getData = (url, selectedSetting) => {
fetch(url)
.then((response) => {
if (response.status !== 200) {
console.log('Looks like there was a problem. Status Code: ' +
response.status);
return;
}
response.json().then((response) => {
//pass formschema here
console.log(selectedSetting);
let newFormSchema = this.setNonDefaultValues(response.data, formSchema.subsections);
Object.assign(this.configSettings, response.data);
this.setState({
configSettings : this.configSettings,
formSchema: newFormSchema
});
});
}
)
.catch((err) => {
console.log('Fetch Error :-S', err);
});
};
componentDidMount() {
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.selected)
})
}
componentDidUpdate() {
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.props.selectedSetting)
})
}
render() {
return (
<div className="card-wrapper">
<h2>{formSchema.label.toUpperCase()}</h2>
{
formSchema.subsections.map((subSection) => {
return (
<>
<h3>{subSection['description']}</h3>
<div style={{marginBottom: '10px'}}></div>
{
subSection['input_fields'].map((inputField) => {
return buildForm(inputField, this.handleChange)
})
}
<hr></hr>
</>
)
})
}
<button className="button button-primary">Save Changes</button>
</div>
)
}
}
The selectedSetting parameter that gets passed to the getData() method in this component will change however and when this changes, I need to change the state of the component and get new data specific to the changed selectedSetting parameter.
The new selectedSetting is passed into the component as a prop. The problem is that I can't pass the new selectedSetting parameter to my getData method to update the state of the component as it gets caught in an infinite loop.
How do I go about passing the new selectedSetting to the getData() method without getting caught in an infinite loop? Is this even possible? If not, what is the best approach I should take?
note the selectedSetting parameter isn't used in the getData() function yet but will be and it will be used to get data from an API call and a new form schema which will then lead to the ConfigSettings and formSchema states being changed
If you look closely on the lifecycle of your component, after mount, you'll fetch then update the component. This will trigger the componentDidUpdate lifecycle method which will do the same thing, causing the infinite loop. You need to have a flag that checks whether this.props.selected changed. If it didn't, don't fetch the data else fetch as normal. In the update method, you have access to the previous props. (You may also do this in componentShouldUpdate method, but it'll be just outright risky)
componentDidUpdate(prevProps) {
if( prevProps.selectedSetting !== this.props.selectedSetting ){
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.props.selectedSetting)
})
}
}
also just a heads up, I noticed that your didMount method, uses a default of "general" as the selected setting, since you want to be using this.props.selectedSetting might be better if it was the one being used instead and just set default props to "general".

How to replace componentDidMount with getderivedstatefromprops?

I want to use getderivedstatefromprops to update the state of selectedPlan when it receives a different number from the prop defaultPlan.
I have tried to convert componentDidMount to getderivedstatefromprops, but getderivedstatefromprops does not seem to be getting called in my component?
state = {
selectedPlan: 0,
};
static getderivedstatefromprops(props, state){
if (props.defaultPlan !== state.selectedPlan) {
return{
selectedPlan: props.defaultPlan
};
}
return null;
}
componentDidMount() {
if (this.props.plans.length > this.props.defaultPlan) {
this.setState({ selectedPlan: this.props.defaultPlan });
}
}
I am using this in a PureComponent, and I'm not sure if thats the reason why it is not working?

getDerivedStateFromProps, change of state under the influence of changing props

I click Item -> I get data from url:https: // app / api / v1 / asset / $ {id}. The data is saved in loadItemId. I am moving loadItemId from the component Items to the component Details, then to the component AnotherItem.
Each time I click Item the props loadItemId changes in the getDerivedStateFromProps method. Problem: I'll click Element D -> I see in console.log 'true', then I'll click Element E --> It display in console.log true andfalse simultaneously, and it should display only false.
Trying to create a ternary operator {this.state.itemX ['completed'] ? this.start () : ''}. If {this.state.itemX ['completed'] call the function this.start ()
Code here: stackblitz
Picture: https://imgur.com/a/OBxMKCd
Items
class Items extends Component {
constructor (props) {
super(props);
this.state = {
itemId: null,
loadItemId: ''
}
}
selectItem = (id) => {
this.setState({
itemId: id
})
this.load(id);
}
load = (id) => {
axios.get
axios({
url: `https://app/api/v1/asset/${id}`,
method: "GET",
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
this.setState({
loadItemId: response.data
});
})
.catch(error => {
console.log(error);
})
}
render () {
return (
<div >
<Item
key={item.id}
item={item}
selectItem={this.selectItem}
>
<Details
loadItemId={this.state.loadTime}
/>
</div>
)
}
Item
class Item extends Component {
render () {
return (
<div onClick={() => this.props.selectItem(item.id}>
</div>
)
}
}
Details
class Details extends Component {
constructor() {
super();
}
render () {
return (
<div>
<AnotherItem
loadItemId = {this.props.loadItemId}
/>
</div>
)
}
}
AnotherItem
class AnotherItem extends Component {
constructor() {
super();
this.state = {
itemX: ''
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.loadItemId !== prevState.loadItemId) {
return { itemX: nextProps.loadItemId }
}
render () {
console.log(this.state.itemX ? this.state.itemX['completed'] : '');
{/*if this.state.loadX['completed'] === true, call function this.start()*/ }
return (
<button /*{this.state.loadX['completed'] ? this.start() : ''}*/ onClick={this.start}>
Start
</button>
);
}
}
here:
selectItem = (id) => {
this.setState({
itemId: id
})
this.load(id);
}
you call setState(), then 'Item' and 'Details' and 'AnotherItem' call their render method. so you see log for previous 'loadItemId'.
when 'load' method work done. here:
this.setState({
loadItemId: response.data
});
you setState() again, then 'Item' and 'Details' and 'AnotherItem' call their render method again. in this time you see log for new 'loadItemId'.
solution
setState both state in one place. after load method done, instead of:
this.setState({
loadItemId: response.data
});
write:
this.setState({
itemId: id,
loadItemId: response.data
});
and remove:
this.setState({
itemId: id
})
from 'selectItem' method.
Need some clarification, but think I can still address this at high level. As suggested in comment above, with the information presented, it does not seem that your component AnotherItem actually needs to maintain state to determine the correct time at which to invoke start() method (although it may need to be stateful for other reasons, as noted below).
It appears the functionality you are trying to achieve (invoke start method at particular time) can be completed solely with a comparison of old/new props by the componentDidUpdate lifecycle method. As provided by the React docs, getDerivedStateFromProps is actually reserved for a few 'rare' cases, none of which I believe are present here. Rather, it seems that you want to call a certain method, perhaps perform some calculation, when new props are received and meet a certain condition (e.g., not equal to old props). That can be achieved by hooking into componentDidUpdate.
class AnotherItem extends Component {
constructor(props) {
super(props);
this.state = {}
}
start = () => { do something, perform a calculation }
// Invoked when new props are passed
componentDidUpdate(prevProps) {
// Test condition to determine whether to call start() method based on new props,
// (can add other conditionals limit number of calls to start, e.g.,
// compare other properties of loadItemId from prevProps and this.props) .
if (this.props.loadItemId && this.props.loadItemId.completed === true) {
//Possibly store result from start() in state if needed
const result = this.start();
}
}
}
render () {
// Render UI, maybe based on updated state/result of start method if
// needed
);
}
}
You are encountering this behaviour because you are changing state of Items component on each click with
this.setState({
itemId: id
})
When changing its state, Items component rerenders causing AnotherItem to rerender (because that is child component) with it's previous state which has completed as true (since you've clicked element D before). Then async request completes and another rerender is caused with
this.setState({
loadItemId: response.data
});
which initiates another AnotherItem rerender and expected result which is false.
Try removing state change in selectItem and you'll get desired result.
I'd suggest you read this article and try to structure your code differently.
EDIT
You can easily fix this with adding loader to your component:
selectItem = (id) => {
this.setState({
itemId: id,
loading: true
})
this.load(id);
}
load = (id) => {
axios.get
axios({
url: `https://jsonplaceholder.typicode.com/todos/${id}`,
method: "GET"
})
.then(response => {
this.setState({
loading: false,
loadItemId: response.data
});
})
.catch(error => {
console.log(error);
})
}
render() {
return (
<div >
<ul>
{this.state.items.map((item, index) =>
<Item
key={item.id}
item={item}
selectItem={this.selectItem}
/>
)
}
</ul>
{this.state.loading ? <span>Loading...</span> : <Details
itemId={this.state.itemId}
loadItemId={this.state.loadItemId}
/>}
</div>
)
}
This way, you'll rerender your Details component only when you have data fetched and no unnecessary rerenders will occur.

React component ignore displaying text for empty obj

I have a react component with a method that reads some data from an object and then returns the text on its status on another component.
The problem is that when the application is loading at first the obj length will be 0 initially and when the object data is loaded it will then
update and the obj.length will be higher than 0.
The what happens is this:
Scenario 1 (obj will actually have data)
-- App Loading
-- getTxt checks if obj is empty
-- As data is not loaded yet on the first call obj.length will return 0 so 'No Data Found' is displayed
-- Then when the app finishes loading it then updates and the obj.length > 0 so 'Found the data' is displayed
or
Scenario 2 (obj will be empty)
-- App Loading
-- getTxt checks if obj is empty
-- As data is not loaded yet on the first call obj.length will return 0 so 'No Data Found' is displayed
-- Then when the app finishes loading it then updates and the obj is actually empty so it can stay the same.
My problem is that if after the app is loaded and obj.length re-checked it returns > then 0 then I don't want to display the first 'No Data Found',
but I need to have the condition just in case after the app has finished loading the data the data is still = 0
Here is the code:
import React from 'react'
class MyComponent extends React.Component {
getTxt() {
if (this.props.obj.length > 0) {
return 'Found the data';
} else if (this.props.obj.length == 0) {
return 'No Data Found';
}
return 'Searching Data'
}
render() {
return <SomeComponent text={this.getTxt()}/>
}
}
export {MyComponent}
What can I do in order to get this done?
As stated in the comments by Felix Kling.
You have three different states.
Initial state
No data
There is some data
Initial state will have the data props set to null. After the data was received it could be an empty object {} or an object with some information.
I would probably write this in the following way:
class MyComponent extends React.Component {
getStatus() {
const { data } = this.props;
if (data === null) {
return "Searching data"
}
if (Object.keys(data).length === 0) {
return "No data was found"
}
return "Found some data"
}
render() {
return <SomeComponent text={this.getStaus()}/>
}
}
This is almost perfect because I would like to separate logic from the view. I will have a reducer that will get the data and upon its values/length it will determine the status.
const statusReducer = (data) => {
if (data === null) {
return "Searching data"
}
if (Object.keys(data).length === 0) {
return "No data was found"
}
return "Found some data"
}
class MyComponent extends React.Component {
state = { data: null }
componentDidMount() {
getData()
.then(resp => resp.json)
.then(resp => this.setState({data: resp.data})
}
render() {
return <SomeComponent text={statusReducer(this.state.data)} />
}
}
You may ask what's the point of passing this to another function (reducer)? First, as I mentioned, separating the logic from the view. Second, statusReducer can be reused in other components. Third, easier to test the code.
NOTE: We only took care of the happy path. However, if there was a problem with the request or the response contains error, we probably get the wrong status.
To handle such case lets look at a different approach. Lets have data and status in state
class MyComponent extends React.Component {
state = { data: null, status: 'PENDING' }
componentDidMount() {
getData()
.then(resp => resp.json)
.then(resp => this.setState({data: resp.data, status: 'COMPLETE'})
.catch(err => this.setState(data: err.Message, status: 'ERROR'})
}
render() {
switch(this.state.status) {
case 'COMPLETE':
return <SomeComponent text={statusReducer(this.state.data)} />
case 'ERROR':
return <ErrorComponent message={this.state.data} />
case 'PENDING':
return <Spinner />
default:
// there is a runtime/compiling error.
// Notify it in you console.log or something
return null // Here I am silently failing but that's not a good practice
}
}
}
We could do even better if we move the switch-case statement to another function and call it loadReducer. But I will let you decide whether to do it or not.
Please notice Eitan's solution too. He uses both componentDidMount and componentWillUpdate to update status.
It sounds like you don't want to render anything until the data is received?
What I do when I need to do this is use component state to keep track of loading status. When the component mounts, set state.loading to true.
componentDidMount() {
this.setState({loading: true})
}
Set loading to false once everything is updated.
componentDidUpdate() {
if (this.state.loading) {
this.setState({loading: false})
}
}
And in the render function, conditionally render based on loading state.
render() {
const text = this.state.loading ? "Loading..." : this.getTxt()
return <SomeComponent text={text}/>
}
I agree with Felix in the comments. You may be better off with a nested ternary in a functional component. The component should update whenever props to it obj are updated.
export const myComponent = (props) => {
return (
<SomeComponent
text={ props.obj ?
props.obj.length > 0 ?
'Found Data'
:
'No Data Found'
:
'Searching'
}
/>
)
}

React - Axios/Fetch/Ajax request after new props passed to child component

I have two components, App and Child. My goal is that App passes an id down to Child and when Child receives it as props, I want Child to perform an Axios/Fetch request and then update itself with the result data. I will accept any answer that provides me with an example App/Child source code that completes my example or gives my insight if this is actually possible what I'm trying to implement.
I am a new to React.
// App.js
import React, { Component } from 'react';
import Child from './Child.js';
import './App.css';
class App extends Component {
state = {
id: 0
};
handleClick() {
this.setState({
id: this.state.id + 1
});
}
render() {
return (
<div>
<Child id={this.state.id} />
<div>
<button onClick={this.handleClick.bind(this)}>Pass new id down to Child component</button>
</div>
</div>
);
}
}
export default App;
// Child.js
import React, { Component } from 'react';
import axios from 'axios';
class Child extends Component {
state = {
data: null,
id: 0
};
loadData(q, cb) {
axios.get('http://localhost:3000/foo?q='+this.state.id)
.then(result => {
// ?
// this.setState would retrigger update and create an infinite updating loop
})
.catch(error => {
console.log('error: ' + error);
});
}
shouldComponentUpdate(nextProps, prevState) {
console.log('shouldComponentUpdate: nextProps = ' + JSON.stringify(nextProps) + ', prevState = ' + JSON.stringify(prevState));
// ??
}
getSnapshotBeforeUpdate(nextProps, prevState) {
console.log('getSnapshotBeforeUpdate: nextProps = ' + JSON.stringify(nextProps) + ', prevState = ' + JSON.stringify(prevState));
// ??
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log('getDerivedStateFromProps: nextProps = ' + JSON.stringify(nextProps) + ', prevState = ' + JSON.stringify(prevState));
return {
id: nextProps.id
};
// ??
}
componentDidUpdate() {
console.log('componentDidUpdate');
}
render() {
return (
<div>
<div>data = {this.state.data}</div>
</div>
);
}
}
export default Child;
You should call check prev Id with current id to avoid recursive update. You just need to make sure you only derive state from props if your props have changed,
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.id !== prevState.id) {
return {
id: nextProps.id
};
}
return null;
}
Let me know if your issue still persists
This could be one way to go about your problem,
Firstly, you'll have to update all the state data inside your getDerivedStateFromProps
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.id !== prevState.id) {
// Returning this object is equivalent to setting state using this.setState
return {
id: nextProps.id
data: loadData(nextProps.id) // Something like this
};
}
return null; // Indicates no state change
}
As for the api call inside loadData method you can make use of the aync / await to return the data from api call
async loadData(id) {
try {
// Make your api call here
let response = await axios.get('http://localhost:3000/foo?q='+id);
return await response.json();
} catch(err) {
// catches errors both in axios.get and response.json
// Make sure to not update state in case of any error
alert(err);
return null;
}
}
Note:
This is not the complete solution as you might want to not update the state in the case where your api call in loadData catches any error.
Also, since you are using api calls, you might want to limit the times pass props to the child cause they all will trigger different api calls and you will run into unpredictable states. Try debouncing.

Categories

Resources