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

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.

Related

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

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

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.

setState error on unmounted component when with data from Firebase

When the component below is mounted, everything Firebase related works fine. The issue occurs when the data in Firebase is updated. I then navigate to a different route, therefore un-mounting this component and the setState error occurs.
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component
I have tried turning the Firebase functions 'off' in componentWillUnmount by i still seem to be hit with the error. Any help would be appreciated
constructor() {
super();
this.state = {
firebaseData: {}
};
}
componentDidMount() {
const { referenceId } = this.props.episode || '';
if (referenceId) {
this.getFirebaseData(this.removeDissallowedChars(referenceId));
}
}
componentWillReceiveProps(nextProps) {
if (this.props.values.referenceId !== nextProps.values.referenceId) {
this.setState({
referenceId: nextProps.values.referenceId,
}, this.fetchWorkflows);
}
}
getFirebaseData(refId) {
const database = firebase.database().ref(`workflows/sky/${refId}`);
database.on('value', snapshot => {
this.setState({ firebaseData: snapshot.val() });
}, error =>
console.log(error)
);
}
componentWillUnmount(refId) {
const database = firebase.database().ref(`workflows/sky/${refId}`);
database.off();
}
removeDissallowedChars(badRefId) {
/**
* BE strip characters that Firebase doesn't allow.
* We need to do the same. Reference id will only contain the characters listed below.
* Change needed in FE as some of our reference id's currently contain period characters.
**/
return badRefId.replace(/[^A-Za-z0-9-:/]+/g, '-');
}
fetchWorkflows() {
const { referenceId } = this.state;
this.props.fetchWorkflows(referenceId);
}
You can have a class variable that keeps track of whether or not your component is mounted. That would look like this:
constructor() {
//...
this._mounted = false;
}
componentDidMount() {
this._mounted = true;
//...
}
componentWillUnmount() {
//...
this._mounted = false;
}
Then on any place you set the state after an async request, you can put an if statement that checks whether or not _mounted is true.
In your case:
getFirebaseData(refId) {
const database = firebase.database().ref(`workflows/sky/${refId}`);
database.on('value', snapshot => {
// Check if component is still mounted.
if (this._mounted) {
this.setState({ firebaseData: snapshot.val() });
}
}, error =>
console.log(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);

When use ComponentDidMount() I found this error : Can't call setState

I found this error :
Can't call setState (or forceUpdate) 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.
Context : When I'm connected, I'm on the homepage, this page not contain the breadCrumb, but If I go on CampaignPage (also the name of the component), I have the BreadCrumb (Component name) I found this error.
On other post what I could see, they said probably problem on asynchronously on ComponentWillMount but I think my problem is different and I can't find a solution.
My code look like that :
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classnames from 'classnames';
import objectAssign from 'object-assign';
import { withRouter } from 'react-router';
import {
BREADCRUMBS_ROUTES,
BREADCRUMBS_ROUTES_FOR_ID,
BREADCRUMBS_ENDPOINT
} from 'constants';
import { getEntityById, setUpdatedBreadcrumbs } from 'actions/breadcrumbs';
import style from './style.scss';
class Breadcrumbs extends Component {
constructor(props) {
super(props);
this.state = {
breadcrumbs: [],
names: {}
};
this.setBreadcrumbs = this.setBreadcrumbs.bind(this);
this.loadEntityNameById = this.loadEntityNameById.bind(this);
}
componentWillMount() {
this.setBreadcrumbs();
}
componentWillReceiveProps(nextProps) {
const { isWillUpdate: newIsWillUpdate } = nextProps;
const { isWillUpdate, saveUpdatedBreadcrumbs } = this.props;
if (isWillUpdate === false && newIsWillUpdate === true) {
this.setBreadcrumbs();
saveUpdatedBreadcrumbs();
}
}
setBreadcrumbs() {
const { params, path } = this.props.match;
let currentPath = '';
const pathSplitedAndExtendet = path.split('/')
.filter(item => !!item)
.map(item => {
if (item[0] === ':' && item.slice(1) !== 'adPage') {
const parameterName = item.slice(1);
this.loadEntityNameById(
parameterName,
params[parameterName]
);
return {
route: `/${params[parameterName]}${BREADCRUMBS_ROUTES_FOR_ID[parameterName]}`,
parameter: parameterName
};
}
return {
route: `/${item}`,
parameter: ''
};
});
const breadcrumbs = pathSplitedAndExtendet
.filter(item => item.parameter !== 'adPage')
.map((item) => {
const indexOfRoute = currentPath.indexOf(item.route);
if (currentPath.slice(indexOfRoute) !== item.route) {
currentPath = `${currentPath}${item.route}`;
}
return ({
...item,
name: BREADCRUMBS_ROUTES[item.route] || '',
route: currentPath
});
});
this.setState({ breadcrumbs });
}
async loadEntityNameById(parameter, id) {
const { loadEntityById } = this.props;
await loadEntityById(BREADCRUMBS_ENDPOINT[parameter], id)
.then((data) => {
this.setState({ names: objectAssign(this.state.names, { [parameter]: { id, name: data.name } }) });
});
}
render() {
const { breadcrumbs, names } = this.state;
const { showBreadcrumbs } = this.props;
return (
<div className={style.breadcrumbs}>
{
showBreadcrumbs && breadcrumbs
.map((item, index) => {
return (
<div
key={`${item.name}--${item.route}--${index}`}
className={classnames(style.bread, index === breadcrumbs.length - 1 ? style.last : null)}
role="link"
tabIndex={-10 - index}
onKeyDown={() => {}}
onClick={item.route ? () => this.props.history.push(item.route) : null}
>
{`${item.name || (names[item.parameter]
? names[item.parameter].name : '...')}
${((breadcrumbs.length > 1) && (index !== breadcrumbs.length - 1)) ? ' >' : ''}
`}
</div>
);
})
}
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Breadcrumbs));
Can't call setState (or forceUpdate) 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.
This error message clearly states that your application has memory leak. What's going on here exactly?
Well, you're using async operation (loadEntityNameById) in setBreadcrumbs method. Which is being called in componentWillMount and in componentWillReceiveProps. This means when you go from CampaignPage component to BreadCrumb component, it will do the async operation ie. loadEntityNameById is running in the background which only sets the state once it's finished. But until that time your BreadCrumb component might be unmounted. The react application doesn't allow you to update the state on an unmounted component.
Furthermore, you should not use componentWillMount method at all. Use componentDidMount hook instead.
To fix the issue, what you can do is to set a flag like:
componentDidMount() {
// component is mounted, set the didMount property on BreadCrumb component
// you can use any name instead of didMount what you think is proper
this.didMount = true
// now, you can update the state
if(this.didMount) { // be sure it's not unmounted
this.setState({names: ...})
}
Next, you should clear the didMount property when the component is unmounted.
componentWillUnmount() {
this.didMount = false // component is unmounted
This will ensure you that your application memory will not be leaked. Because, we properly setting the state when required but not when it doesn't require, and stopping unnecessary loop.
You're doing an asynchronous action (loadEntityNameById) that sets the state for the component when it finishes. By that time, your Breadcrumbs component may have been unmounted, and the error is thrown.
You cannot call setState in componentWillMount try using componentDidMount instead

Categories

Resources