How can I avoid this infinite update loop in React.js? - javascript

I have a parent component that has an index property in its state. When the index changes, I want it to pass new data to a child component in its props.
When the child component receives new data, I want it to make an AJAX call to update its own state. I cannot use componentDidUpdate() because this method runs when props or state changes. So, I basically get this happening:
Parent passes new props
Child runs componentDidUpdate()
Child runs fetchFromServer(), which updates state
Child runs componentDidUpdate()
To infinity...
Is there a method for example that only runs when a component's props are updated?
Parent Component
export default class ResultsFrame extends Component {
constructor(props) {
super(props);
this.state = {
resultIndex: 0,
}
}
render() {
return (
<SequenceView data={this.props.results.results[this.state.resultIndex]}/>
);
}
}
Child Component
export default class SequenceView extends Component {
constructor(props) {
super(props);
this.state = {
data: {},
isLoading: true,
}
}
render() {
if(this.state.isLoading)
{
return ( <Spinner /> );
}
else
{
return (
<div>
{/* Render relevant stuff... */}
</div>
);
}
}
componentDidMount()
{
this.fetchFromServer();
}
componentDidUpdate()
{
this.fetchFromServer();
}
fetchFromServer = () =>
{
const sendData = {
data: this.props.data,
};
axios.post(`/analysis/seqview`, sendData)
.then(res => {
const response = res.data;
console.log(response);
this.setState({data: response, isLoading: false});
})
.catch(function (e) {
console.log(e.response.data.message);
});
}
}

From the documentation
You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition like in the example above, or you’ll cause an infinite loop.
This is exactly what you are doing here . your componentDidUpdate calls fetchFromServer which sets the state .
You need to change your componentDidUpdate to
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.data.something !== prevProps.data.something) {
this.fetchFromServer();
}
}
Refer:
https://reactjs.org/docs/react-component.html#componentdidupdate

Related

React memory leak - when updating state in context provider via a function passed to a child of the provider

After some debugging I understand the issue and I know roughly why it's happening, so I will show as much code as I can.
The Error
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
in ProductsDisplay (created by ConnectFunction)
in ConnectFunction (created by Context.Consumer)
in Route (created by SiteRouter)
in Switch (created by SiteRouter)
in SiteRouter (created by ConnectFunction)
in ConnectFunction (created by TLORouter)
in Route (created by TLORouter)
in Switch (created by TLORouter)
So to give you context, the React structure looks a bit like so
Simplified version
App.jsx > Router > GlobalLayoutProvider > Route > Page
Within the GlobalLayoutProvider I pass six functions down via the new react context, the code looks like so. All these functions provide is the ability to modify the state of the layout component, so that if child elements have more complex requirements they can send the information up after performing fetchs etc or they could on mount set the values of the layout.
GlobalLayoutRedux.jsx
class GlobalLayoutProvider extends React.Component {
constructor(props) {
super(props);
this.state = { routeConfig: null };
this.getRouteData = this.getRouteData.bind(this);
this.setLoaderOptions = this.setLoaderOptions.bind(this);
}
componentDidMount() {
this.getRouteData();
}
componentDidUpdate(prevProps) {
const { urlParams, user, layoutSettings } = this.props;
if (
urlParams.pathname !== prevProps.urlParams.pathname
|| user.permissions !== prevProps.user.permissions
) {
this.getRouteData();
}
}
getRouteData() {
const { user, urlParams } = this.props;
const { tlo, site, pathname } = urlParams;
this.setState({
routeConfig: pageConfigs().find(
(c) => c.pageContext(tlo, site, user) === pathname,
),
});
}
setLoaderOptions(data) {
this.setState((prevState) => ({
routeConfig: {
...prevState.routeConfig,
loader: {
display: data?.display || initialState.loader.display,
message: data?.message || initialState.loader.message,
},
},
}));
}
render() {
const { routeConfig } = this.state;
const { children, user } = this.props;
return (
<GlobalLayoutContext.Provider
value={{
setLoaderOptions: this.setLoaderOptions,
}}
>
<PageContainer
title={routeConfig?.pageContainer?.title}
breadcrumbs={[routeConfig?.pageContainer?.title]}
>
<ActionsBar
actionsBarProperties={{ actions: routeConfig?.actionBar?.actions }}
pageTitle={routeConfig?.actionBar?.title}
/>
<SideNav items={routeConfig?.sideNav?.options} selected={routeConfig?.sideNav?.pageNavKey}>
<div id={routeConfig?.sideNav?.pageNavKey} className="Content__body page-margin">
<div id="loader-instance" className={`${routeConfig?.loader?.display ? '' : 'd-none'}`}>
<Loader message={routeConfig?.loader?.message} />
</div>
<div id="children-instance" className={`${routeConfig?.loader?.display ? 'd-none' : ''}`}>
{children}
</div>
</div>
</SideNav>
</PageContainer>
</GlobalLayoutContext.Provider>
);
}
}
export default GlobalLayoutProvider;
Inside the Page.jsx we have a componentDidMount and a componentDidUpdate. The issue seems to stem from calling the parent function and setting the state pretty much at any point prior to updating the state of the child component.
Page.jsx
export default class Page extends Component {
static contextType = GlobalLayoutContext;
constructor(props) {
super(props);
this.state = {
someState: 'stuff'
};
}
componentDidMount() {
this.setActionBarButtons();
this.fetchOrganisationsProducts();
}
async componentDidUpdate(prevProps) {
const { shouldProductsRefresh, selectedOrganisation, permissions } = this.props;
if (
selectedOrganisation?.id !== prevProps.selectedOrganisation?.id
|| shouldProductsRefresh !== prevProps.shouldProductsRefresh
) {
await this.fetchOrganisationsProducts();
}
if (
selectedOrganisation?.id !== prevProps.selectedOrganisation?.id
|| shouldProductsRefresh !== prevProps.shouldProductsRefresh
|| permissions !== prevProps.permissions
) {
this.setActionBarButtons();
}
}
setActionBarButtons() {
const { setActionBarOptions } = this.context;
const actions = [
ActionButtons.Custom(
() => this.setState({ exportTemplateModalIsOpen: true }),
{ title: 'Button', icon: 'button' },
),
];
setActionBarOptions({ actions, title: 'Products', display: true });
}
async fetchOrganisationsProducts() {
const { selectedOrganisation } = this.props;
const { setLoaderOptions } = this.context;
setLoaderOptions({ display: true, message: 'Loading Products In Organisation' });
(await productStoreService.getProducts(selectedOrganisation.id))
.handleError(() => setLoaderOptions({ display: false }))
.handleOk((products) => {
this.setState({ products }, () => {
setLoaderOptions({ display: false });
products.forEach(this.fetchAdditionalInformation)
});
});
}
render() {
return (<p>Something</p>)
}
}
What's odd the memory leak will disappear if I add this suggestion I seen on stack overflow suggesting to track the state of the components interacting with the higher-level component.
export default class Page extends Component {
static contextType = GlobalLayoutContext;
constructor(props) {
super(props);
this.state = {
someState: 'stuff'
};
}
// ADDITION HERE
_isMounted = false;
componentDidMount() {
// ADDITION HERE
this._isMounted = true;
this.setActionBarButtons();
this.fetchOrganisationsProducts();
}
// ADDITION HERE
componentWillUnmount() {
this._isMounted = false;
}
async fetchOrganisationsProducts() {
const { selectedOrganisation } = this.props;
const { setLoaderOptions } = this.context;
setLoaderOptions({ display: true, message: 'Loading Products In Organisation' });
(await productStoreService.getProducts(selectedOrganisation.id))
.handleError(() => setLoaderOptions({ display: false }))
.handleOk((products) => {
// ADDITION HERE
if (this._isMounted) {
this.setState({ products }, () => {
setLoaderOptions({ display: false });
products.forEach(this.fetchAdditionalInformation)
});
}
});
}
render() {
return (<p>Something</p>)
}
}
Personally, I don't see this as a solution if I was building my own thing I wouldn't be too fussed but I can't ask an entire company to start adding this addition everywhere.
My gut is telling me that because the component is firing up an object to configure the state of the parent which is for a fraction of a second unmounting as the component did mount is still processing due to the async network fetch when that is returned it is saving to the state before the parent has managed to render the function call state change.
What was odd if I pass the callbacks into the parent and call them once the setState has been actioned the issue is resolved like so
setOnMountOptions(data) {
this.setState((prevState) => ({
routeConfig: {
...prevState.routeConfig,
...data?.loader ? { loader: data.loader } : {},
},
}), async () => { await data.callbacks(); });
}
but again this causes havoc on the testing side as you are abstracting the componentDidmount functionality out and calling it after a set state is actioned elsewhere.
I have tried adapting what I have to Redux but I had the exact same result everything from a viewing perspective in the browser was fine but still getting the same memory leak using the Redux calls to try to populate all the data from top to bottom.
I can't think of any way of handling this gracefully where we don't need to ask the company to add that fix everywhere.
So to save people time and effort it turns out our memory leak was actually being cause by a bad set state in the routers of our application.

Returning state after API call inside getDerivedStateFromProps

I have a react component that receives props for filtering from its parent. When the parent props change I use getDerivedStateFromProps in the child in the following code:
static getDerivedStateFromProps(props, state) {
if (props.filter === null) {
return state;
}
const { name, description } = props.filter;
ApiService.get("cards", { name: name, description: description })
.then(response => {
console.log("get request returned ", response.data);
return { cards: response.data };
}
);
}
in the console the response.data log is the approriate array of objects. However the state does not update and the rendering function still uses the old cards array and not the one that was received from the ApiService response. How do I make it so the cards array updates properly so that on the next render it will show the filtered cards?
getDerivedStateFromProps is not the correct lifecycle hook for this. You'll need to put the fetching code in componentDidMount and componentDidUpdate, and use this.setState once the data is available.
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
cards: null,
}
}
componentDidMount() {
this.loadData();
}
componentDidUpdate(prevProps) {
if (this.props.filter !== prevProps.filter) {
this.loadData();
}
}
loadData() {
const { name, description } = this.props.filter;
ApiService.get("cards", { name: name, description: description })
.then(response => {
this.setState({ cards: response.data });
});
)
render() {
if (!this.state.cards) {
return <div>Loading...</div>
}
return (
// some jsx using the cards
)
}
}

Why is this function call in render() creating an infinite loop?

I want to call a function in render() which will update the state. But when I do that, it gives me this error message:
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.
I don't understand why this is happening because I'm not directly setting the state in my render(), I'm setting it in my setInsightUrl() function.
I've tried using different lifecycle functions but couldn't get any to work. I'm not sure how else to write this function.
import React, { Component } from 'react'
import "../../css/tabs/Tabs.css"
import {connect} from "react-redux"
class InsightPage extends Component {
constructor() {
super();
this.state = {
insightUrlState: null
}
this.setInsightUrl = this.setInsightUrl.bind(this);
}
setInsightUrl(url) {
this.setState({
insightUrlState: url
})
console.log(this.state.insightUrlState, 'INSIGHTTTTTT URLLLLLbgnhjm,k.l')
}
render() {
this.props.sideTreeMenu.dynamicMenu.forEach(obj => {
obj.children.forEach(child => {
child.insights.forEach(insight => {
if (insight.insightName === this.props.insightNameReducer) {
{this.setInsightUrl(insight.insightURL)}
}
})
})
})
return (
<div className={this.props.drawerOpen ? "tab_container2" : "tab_container" }>
<h1>Hello from Insight</h1>
<iframe frameBorder="0" style={{width: "100%", height: "70vh"}} src="https://insighttbdashboards.verizon.com/t/DigtalAnalytics/views/Digital_Analytics/Chat_Dashboard?iframeSizedToWindow=true&:embed=y&:showAppBanner=false&:display_count=no&:showVizHome=no#2" />
</div>
)
}
}
const mapStateToProps = state => ({
drawerOpen: state.SideDrawerReducer.open,
sideTreeMenu: state.SideDrawerReducer.menu,
insightNameReducer: state.SideDrawerReducer.insightName
})
export default connect(mapStateToProps)(InsightPage);
It should update the state with the url I am passing into the function in the render block.
Just because you are calling setState in a function defined outside of render (setInsightUrl) doesn't mean you aren't calling it within render, render potentially calls setInsightUrl when the right conditions are met, and thus can potentially loop forever.
Perhaps you could update the state only if it actually is changing:
setInsightUrl(url) {
if (this.state.insightUrlState != url) {
this.setState({
insightUrlState: url
})
console.log(this.state.insightUrlState, 'INSIGHTTTTTT URLLLLLbgnhjm,k.l')
}
}
From the code you posted (I'm not sure if that is the full code for your component) there's no need to determine the insight url in the render() function. If you do want to determine it in the render function (which should be the last thing your component does) then you shouldn't need to put it in the state, you should just use a local variable for it.
But if you want it in the state, you can either do it in the constructor:
constructor(props) {
super(props);
let insightUrlState = null;
props.sideTreeMenu.dynamicMenu.forEach(obj => {
obj.children.forEach(child => {
child.insights.forEach(insight => {
if (insight.insightName === props.insightNameReducer) {
insightUrlState = insight.insightURL;
}
});
});
});
this.state = { insightUrlState };
}
With an additional use of a lifecycle method if you want to update the state when the props change:
componentDidUpdate(prevProps, prevState) {
// depending on how many items are in these arrays, you might want to
// wrap this in a check to see if this.props.sideTreeMenu.dynamicMenu has
// changed from prevProps.sideTreeMenu.dynamicMenu and/or if
// this.props.insightNameReducer has changed from prevProps.insightNameReducer
let insightUrlState = null;
this.props.sideTreeMenu.dynamicMenu.forEach(obj => {
obj.children.forEach(child => {
child.insights.forEach(insight => {
if (insight.insightName === this.props.insightNameReducer) {
insightUrlState = insight.insightURL;
}
});
});
});
if (prevState.insightUrlState !== insightUrlState) {
this.setState({ insightUrlState });
}
}
Or, alternatively, you can use the getDerivedStateFromProps function to determine the insightUrlState value just before rendering (using this function, you don't need to use the constructor or componentDidUpdate options):
static getDerivedStateFromProps(props) {
let insightUrlState = null;
props.sideTreeMenu.dynamicMenu.forEach(obj => {
obj.children.forEach(child => {
child.insights.forEach(insight => {
if (insight.insightName === props.insightNameReducer) {
insightUrlState = insight.insightURL;
}
});
});
});
return { insightUrlState };
}
this.props.sideTreeMenu.dynamicMenu.forEach(obj => {
obj.children.forEach(child => {
child.insights.forEach(insight => {
if (insight.insightName === this.props.insightNameReducer) {
{this.setInsightUrl(insight.insightURL)}
}
})
})
})
This block is not valid JSX, you might need to move that to componentDidMount.
You can't call setState inside render, otherwise will cause a re-render, so it will go again to render and so on... That's why you got that error.

API taking too long, map function firing before data loads

import React, { Component } from 'react';
import {withProvider} from './TProvider'
import ThreeCardMap from './ThreeCardMap';
class Threecard extends Component {
constructor() {
super();
this.state = {
newlist: []
}
}
componentDidMount(){
this.props.getList()
this.setState({newlist: [this.props.list]})
}
// componentDidUpdate() {
// console.log(this.state.newlist);
// }
render() {
const MappedTarot = (this.state.newlist.map((list, i) => <ThreeCardMap key={i} name={list.name} meaningup={list.meaning_up} meaningdown={list.meaning_rev}/>);
return (
<div>
<h1>Three Card Reading</h1>
<div>{ MappedTarot }</div>
</div>
)
}
}
export default withProvider(Threecard);
Hi, I'm trying to create a page that takes data from a tarot card API (https://rws-cards-api.herokuapp.com/api/v1/cards/search?type=major). Unfortunately by the time the data comes in, my map function has already fired. I'm asking to see if there is a way to have the map function wait until the data hits before it fires. Thanks!
Edit: getList function in the Context:
getList = () => {
console.log('fired')
axios.get('https://vschool-cors.herokuapp.com?url=https://rws-cards-api.herokuapp.com/api/v1/cards/search?type=major').then(response =>{
this.setState({
list: response.data
})
}).catch(error => {
console.log(error);
})
}
this.props.getList() is an async function. You are setting the list right after that call which is not correct.
You need to set it in the getList promise then() block.
getList() is an async function and update data for the parent component. So, my solution is just watching the list from the parent component if they updated or not, through getDerivedStateFromProps
class Threecard extends Component {
constructor() {
super();
this.state = {
newlist: []
}
}
// Set props.list to this.state.newList and watch the change to update
static getDerivedStateFromProps(nextProps, prevState) {
return {
newlist: nextProps.list
}
}
componentDidMount(){
this.props.getList()
// Removed this.setState() from here.
}
render() {
const MappedTarot = (this.state.newlist.map((list, i) => <ThreeCardMap key={i} name={list.name} meaningup={list.meaning_up} meaningdown={list.meaning_rev}/>);
return (
<div>
<h1>Three Card Reading</h1>
<div>{ MappedTarot }</div>
</div>
)
}
}
export default withProvider(Threecard);

pass object from state as props to child component

I am creating weather app with React and I have an api which provides me object
import React, { Component } from 'react';
import WeeklyWeather from './components/WeeklyWeather';
const api = '';
class App extends Component {
constructor() {
super();
this.state = {
weather: {}
}
}
componentDidMount() {
fetch(api)
.then(response => response.json())
.then(data => this.setState({ weather: data }));
}
render() {
return (
<div className="App">
<WeeklyWeather day={this.state.weather.daily.data} />
</div>
);
}
}
After fetching it, I store data as state.
Finally, I want to pass this.state.weather.daily.data as props to child component, but I received TypeError: Cannot read property 'data' of undefined
<WeeklyWeather day={this.state.weather.daily.data} />
It can be you get the error because before the async request has finished, there is no this.state.weather.daily initialized. Quick hack could be something like this:
{ this.state.weather.daily && <WeeklyWeather day={this.state.weather.daily.data} />}
This will make sure WeeklyWeather is only rendered when daily is initialized.
the first render this.state.weather isn't initialized yet
follow this thread
React state data null for the first time
class App extends Component {
constructor () {
super()
this.state = {
weather: {}
}
}
componentDidMount () {
fetch(proxy)
.then(response => response.json())
.then(data => this.setState({ weather: data }))
}
render () {
return (
<div className='App'>
{this.state.weather && (
<WeeklyWeather day={this.state.weather.daily.data} />
)}
</div>
)
}
}
You're passing the prop to the child before it exists.
componentDidMount() is called once the HTML is bound to the DOM, which means your render() has already run. But, of course, your render() refers to this.state.weather.daily.data which doesn't exist until after componentDidMount() completes.
All you have to do is check that the data is loaded before you attempt to use it.
<WeeklyWeather day={this.state.weather.daily && this.state.weather.daily.data} />

Categories

Resources