Setting state setState() inside ComponentDidUpdate() going to infinite loop? - javascript

If someone can help me with this ?
I need this function to store the filteredObject key in the state. but when I call this function in componentDidMount(), it didn't work and when I called it in ComponentDidUpdate() it works but going on a infinite loop?
userData = () => {
const returnedEmail = storageManger.getEmailFromStore();
const { agents } = this.state;
if (returnedEmail) {
const filteredEmail = agents.find(agent => { return agent.email === returnedEmail })
if (filteredEmail) {
this.setState({
agentApplicationId: filteredEmail.properties
})
}
}
}

You need to be very careful when setting state in componentDidUpdate. Calling setState updates the component, which triggers componentDidUpdate, which calls setState, and so on, causing the infinite loop. From the React docs:
You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition...or you’ll cause an infinite loop.
The solution is to add some kind of condition so you're not updating state unnecessarily. For example:
userData = () => {
const returnedEmail = storageManger.getEmailFromStore();
const { agents, agentApplicationId } = this.state;
if (returnedEmail) {
const filteredEmail = agents.find(agent => agent.email === returnedEmail);
// Add an extra condition here to prevent state from updating if the values are already equal.
if (filteredEmail && filteredEmail.properties !== agentApplicationId) {
this.setState({
agentApplicationId: filteredEmail.properties
});
}
}
}

Related

Can't stop an infinite loop at nested component at useEffect hook

I am trying to curb useEffect related to nested components. Here are the components:
Parent (it receives data from API):
const ListOfLots = (props) => {
const initial = {listLots: props.lots, form: props.form}
const [lots, setLots] = useState(initial);
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
});
return (
<div>
{
lots.listLots.map(function(lot) {
return <Lot key={lot.uuid} lot={lot} procForm={lots.form}/>
})
}
</div>
)
}
Nested:
const Lot = (props) => {
const initial = {currLot: props.lot, form: props.form};
const [lot, setLot] = useState(initial);
let getWinningBid = (offers) => {
for (let i = 0; i < offers.length; i++) {
console.log("BOOM!!!");
if (offers[i].isTrue === true) {
return offers[i].pip;
}
}
}
return (
...
)
}
While I am using no dependencies at parent's useEffect, I have got an infinite invoking of console.log("BOOM!!!"), that is, of course, unacceptable, but my Nested component rerendered. When I try to use the following type of dependencies at useEffect: [], [lots.form], [lots.listLots] or [lots.listLots.length] my Nested component is not rerendered: it stays blank. So the result is the following: I have an infinite useEffect loop or not-working(?) useEffect.
Is there any way in this case to handle the useEffect?
Use
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
}, [props.lots, props.form]);
This triggers the callback only if the value of props.lots, props.form is changed else it won't be triggered on every rerender as in case of no second argument.
A similar question here might help you find better explanations.

React infinite loop in useEffect() with reducer

I have a useEffect() that checks a trigger boolean in state if there is a message sound that should play, and after playing it sets that active message trigger to false in state.
However, the useEffect() goes into an infinite loop crashing the app. Probably because changing the state triggers it again (and again...)
Usually, with useState, this is fairly simple to fix with something like useEffect(() => {logic}, [trigger])
In my case I am not using useState, but I am using a reducer to change state.
Edit: The weird thing is, the reducer sometimes works to modify state, and sometimes it does not. It will execute without errors but the state remains unchanged.
Let me show you my commented code:
import React, { useEffect } from "react";
import { getCachedImage } from "../helpers";
const MessageNotification = (props) => {
const messageImg= getCachedImage("/ui/newmessage.png");
// Function that plays given sound
function playSound(soundFile) {
let audio = new Audio("/audio/messages/" + soundFile);
audio.play();
}
// Check if a Message is set to active. If so, execute logic
useEffect(() => {
// Get messages from state and find the message with "active" set to true
const messagesState = props.state.messages;
const activeMessage = messagesState.find((element) => element.active === true);
if (activeMessage) {
playSound(activeMessage.audio);
// Mark the message as userNotified and set it to inactive in state
let updatedMessagesState = messagesState ;
let index = messagesState.indexOf(activeMessage);
if (~index) {
updatedMessagesState[index].userNotified= true;
updatedMessagesState[index].active = false;
}
/* This is the weird part, the updatedMessagesState is correct,
but the dispatch reducer does not pass it to state.
This does work when I remove the useEffect
(but that gives me a fat red warning in console) */
props.dispatch({ type: "UPDATE_MESSAGES", payload: updatedMessagesState });
}
});
return (
<div>
<img src={"images" + messageImg} alt="message" width="90" height="90"></img>
</div>
);
};
export default MessageNotification;
As you can see, I do not use useState but work with a reducer instead. The solution that I often find which pertains to something like the following is not my solution as far as I can tell:
// Not applicable solution for me, since I use reducer
const [trigger] = useState();
useEffect(() => {
// Logic here
}, [trigger]);
Edit: Since the reducer does not seem to modify state when used in useEffect, let me post its code:
const reducer = (state, action) => {
switch (action.type) {
case "UPDATE_MESSAGES":
return { ...state, messages: action.payload };
default:
throw new Error();
}
};
export default reducer;
Try adding a dependency for your useEffect, such as:
useEffect(() => {
if (activeMessage) {
playSound(activeMessage.audio);
//mark the message as userNotified and set it to inactive in state
let updatedMessagesState = messagesState ;
let index = messagesState.indexOf(activeMessage);
if (~index) {
updatedMessagesState[index].userNotified= true;
updatedMessagesState[index].active = false;
}
props.dispatch({ type: "UPDATE_MESSAGES", payload: updatedMessagesState });
}
}, [activeMessage]);
By not specifying a dependency array, your useEffect will run on EVERY render, hence creating an infinite loop.
Also, you are trying to directly modify a prop (and it is an anti pattern) on this line:
const messagesState = props.state.messages;
Try changing it to this:
const messagesState = [...props.state.messages];
Also, let index = messagesState.indexOf(activeMessage); will not work since messagesState is an object array. To get the index of the active message, try this:
let index = messagesState.map(message => message.active).indexOf(true);
I think if you add props.state.messages as dependency, the problem will fixed. Also if you use only the messagesState and messagesState in useEffect, you should move this variables to that block:
useEffect(() => {
const messagesState = props.state.messages;
const messagesState = messagesState.find((element) => element.active === true);
if (activeMessage) {
playSound(activeMessage.audio);
//mark the message as userNotified and set it to inactive in state
let updatedMessagesState = messagesState ;
let index = messagesState.indexOf(activeMessage);
if (~index) {
updatedMessagesState[index].userNotified= true;
updatedMessagesState[index].active = false;
}
/* This is the weird part, the updatedMessagesState is correct,
but the dispatch reducer does not pass it to state.
This does work when I remove the useEffect
(but that gives me a fat red warning in console) */
props.dispatch({ type: "UPDATE_MESSAGES", payload: updatedMessagesState });
}
}, [props.state.messages]);
// Check if a Message is set to active. If so, execute logic
useEffect(() => {
// Get messages from state and find the message with "active" set to true
const messagesState = props.state.messages;
const activeMessage = messagesState.find((element) => element.active === true);
if (activeMessage) {
playSound(activeMessage.audio);
// Mark the message as userNotified and set it to inactive in state
let updatedMessagesState = messagesState ;
let index = messagesState.indexOf(activeMessage);
if (~index) {
updatedMessagesState[index].userNotified= true;
updatedMessagesState[index].active = false;
}
/* This is the weird part, the updatedMessagesState is correct,
but the dispatch reducer does not pass it to state.
This does work when I remove the useEffect
(but that gives me a fat red warning in console) */
props.dispatch({ type: "UPDATE_MESSAGES", payload: updatedMessagesState });
}
});
your useEffect needs a dependency, if you are not providing dependency in useEffect like in your case it'll always run on every render. Provide [] as second argument in your useEffect or [any state or prop on which this effect depends].

React infinite loop when using useffect with redux

I'm setting a radux state with an object, the get the value from the store and print it on screen
I'm using useState and use useEffect
const AvatarContainer = (props) => {
const [infos, setinfos] = useState(undefined);
useEffect(() => {
console.log('in effects')
setinfos(() => props.userInfos)
}, [props.userInfos]);
props.saveUserInfos(authContext.getCachedUser())
const mapStateToProps = (state) => {
return ({
userInfos: state.reportingActions.userInfos,
})
}
const mapDispatchToProps = (dispatch) => {
return ({
saveUserInfos: (Avatar) => dispatch(saveUserInfos(Avatar))
})
}
reducer
case 'SAVE_USER_INFOS':
return {
...state,
userInfos: Object.assign({}, action.payload.userInfos
}
action
export const saveUserInfos = userInfos => {
return ({
type: 'SAVE_USER_INFOS',
payload: {
userInfos
}
})
}
I get this error
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
You need to call redux action in useEffect, like below,
useEffect(() => {
console.log("effect done")
if (photo === undefined) {
setinfos(props.userInfos)
}
props.saveUserInfos(authContext.getCachedUser())
}, []);
Your code goes in infinite loop because props.saveUserInfos(authContext.getCachedUser()) update your component props and when your component's props is update then it will be remount or re-render again
Hope this answer helps you!
props.saveUserInfos(authContext.getCachedUser()) is executed each time the component re-renders. It triggers a redux action dispatch that manipulates redux store which in turn triggers re-render of AvatarContainer component. Then, saveUserInfos is executed so the process repeats and effectively enters infinite loop.
To resolve your issue you need to change the logic when the actual save is done. A starting point - depending on what you want to achieve - might be to move into into effect that triggers on internal state change:
useEffect(() => {
props.saveUserInfos(authContext.getCachedUser())
, [infos]);
This way, external store's state will be synchronized each time internal state changes.

How to call a function that uses the state (which is set by another function) and updates state

I have two functions one that fetches data from an api and updates state according to that data, and a function that itterates over the data in the state and updates the state with the new data.
My problem is that i cant update the state in the second function. And i dont know where i have to call this function in order for it to be called after the first function and to use the data thats in the state.
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
});
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
addPhotos() {
const newCases = this.state.cases.map(({ photo_id, ...rest }) => {
const obj = { ...rest };
this.state.photos.find(data => {
if (data.id === photo_id) {
obj.file = data.file;
return true;
}
});
return obj;
});
console.log(this.state.cases);
this.setState({
'cases' : newCases
});
}
renderCases() {
this.addPhotos();
}
componentWillMount() {
this.getCases();
}
render() {
return (
<div>
{this.renderCases()}
</div>
)
}
}
This is what i now have
Where should i call the addPhotos function so it can update the state and still use the existing state data from the getCases function?
Thanks in advance!
So, first thing's first. The lifecycle method componentWillMount() is soon to be deprecated and is considered unsafe to use. You should be using componentDidMount().
As far as using the updated state in your addPhotos function, you can pass setState a callback function. A seemingly simple solution would be to just pass the addPhotos function as a callback into the setState being called in your getCases function.
getCases() {
axios
.get('/cases/api')
.then(response => {
this.setState({
cases: response.data.cases,
photos: response.data.photos,
}, this.addPhotos);
console.log(this.state.cases);
})
.catch((error) => {
if (error) {
console.log(error);
}
});
}
Another solution would be to call addPhotos() from componentDidUpdate instead.
Hope this helps!
Edit: Just some additional background information from the React docs.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
Added some refactoring to your code, should work ok now, read comments for details
export default class Cases extends Component {
constructor(props) {
super(props);
this.state = {
cases: [],
photos: [],
};
this.addPhotos = this.addPhotos.bind(this);
this.getCases = this.getCases.bind(this);
this.renderCases = this.renderCases.bind(this);
}
getCases() {
axios
.get('/cases/api')
.then(this.addPhotos) // don't need to set state, pass data to consumer function
.catch(console.error); // catch always gives error, don't need to check with if statement
}
addPhotos(response) {
const cases = response.data.cases // extract values
const photos = response.data.photos // extract values
// your .map iterator has O^2 complexity (run each item of cases on each item of photos)
// this new .map iterator has O complexity (run each item of cases)
const newCases = cases.map(({ photo_id, ...rest }) => {
const obj = {...rest};
const data = photos.find(item => item.id === photo_id);
if (data) {
obj.file = data.file
}
return obj;
});
this.setState({
cases: newCases,
photos
});
}
componentDidMount() { // better use didMount
this.getCases();
}
render() {
return (<div />)
}
}

cancel setInterval in componentDidMount on props render

How can I cancel setInterval if this.props.firebase is not equal to null?
VerifyEmail = () => {
const { firebase } = this.props;
console.log(firebase);
if (firebase !== null) {
this.setState({ validCode: true, verifiedCode: true });
}
};
componentDidMount() {
const { firebase } = this.props;
if (firebase === null) {
this.intervalID = setInterval(() => this.VerifyEmail(), 1000);
} else if (firebase !== null) {
clearInterval(this.intervalID);
}
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
Currently, this.VerifyEmail is repeated and my attempt to clearInterval if this.props.firebase is not equal to null does not run.
Also, if you have another method that will automatically action this.VerifyEmail() on page render and when this.props.firebase is not equal to null. Please suggest.
componentDidMount(or cDM in short) runs just once when component is mounted. Later when you change props provided it is not called. Upon props is changed componentDidUpdate is run. You may place your check there keeping in mind to check prevProps.firebase !== this.props.firebase or you may stack in endless re-rendering. Just like that
componentDidUpdate(prevProps) {
const { firebase: prevFirebase } = this.prevProps;
const { firebase } = this.props;
if (!prevFirebase && firebase) { // null => not-null
this.intervalID = setInterval(() => this.VerifyEmail(), 1000);
}
if (prevFirebase && !firebase) { // not-null => null
clearInterval(this.intervalID);
}
}
Don't miss placing the same logic in componentDidMount(for case when props.firebase has been provided initially) and componentDidUpdate(for case when props.firebase is initially null and later is assigned to props).
But I believe there could be more DRY solution. Do you really need to render this component until data is retrieved? Do you render some placeholder until firebase appears and data is loaded? If yes, then how about lift up data loading to parent component? Then your component would retrieve data through props and it might have render as simple as (this.props.data && ...)

Categories