State update on unmounting component - how to fix that? - reactjs

I have some problems with updating state on an unmounted component as below:
All works as expected. Modal is closed after the comment is deleted. But I'm getting a warning:
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
I know what causes the problem but I don't know how to fix that. Here's my data flow:
This function invokes an action responsible for deleting user posts:
async handleDelete() {
const { articleID, comment, deleteComment } = this.props;
await deleteComment({ articleID, idcomment: comment.idcomment });
//showModal flag is set to false - hide confirmation modal after clicking 'yes' button
this.setState({showModal: !this.state.showModal})
}
Action listed above 'deleteComment' looks like this:
export const deleteComment = data => async dispatch => {
const { idcomment, articleID } = data;
try {
await axios.post(`/api/comments/${idcomment}`, {_method: 'DELETE'});
//fetchling comments again to update a state and it looks like it causes the problem with updating a state on unmounted component because when I commented out that line, it didnt happen anymore.
await dispatch(fetchComments(articleID));
}
catch(e) {
throw new Error(e) }
finally {
dispatch(setCommentStatus(true));
dispatch(decCommentCount());
}
}
Not my question is, how to fix that? I want to close my confirmation modal after the comment is deleted from the database and the new sets of comments are already updated.
Here is how I use my modal:
<Modal
showModal={showModal}
accept={this.handleDelete}
denied={() => this.setState({showModal: !this.state.showModal})}
/>
And the last one is the modal itself:
return (
!showModal
? ''
: (
<Wrapper>
<ModalSection>
<Header>
<Title>Usunięcie komentarza</Title>
<ButtonExit onClick={denied}>❌</ButtonExit>
</Header>
<Text>Czy jesteś pewien, że chcesz usunąć ten komentarz? <br /> Nie będziesz mógł cofnąć tej operacji.</Text>
<Footer>
<div>
<Button onClick={denied}>Cofnij</Button>
<Button warning onClick={accept}>Usuń</Button>
</div>
</Footer>
</ModalSection>
</Wrapper>
)
)

Apparently it works after deleting this.setState({showModal: !this.state.showModal}) from handleDelete function. Weird thing is after finishing deleting comment my modal is closing and I don't know HOW.. Something changing showModal value to false... Still figuring out.
EDIT: IM DUMB. After deleting comment I'm fetching comment list again AND that component has state of showModal set to FALSE by default so when component is rerendering the state is default which is false.................. SORRY.

It is because you are trying to modify state of Modal component after its being closed. Instead you should maintain state of comment deletion in the Parent component which renders the Modal component instead. And update the state in Parent component using a callback. This callback can be passed to Modal component as a prop and called when a delete action occurs. This should fix the issue.

You shouldn't use tenary to render the modal (!showModal ? '' : <...>) You should pass a prop to your modal that only hides or shows the modal.
If you see some libraries they have a show prop or isOpen
e.g. react-bootstrap react-modal
They have that prop so you don't have errors like the one you are having.

Related

React useState reruns the component function on no change

in this very simple demo
import { useState } from 'react';
function App() {
const [check, setCheck] = useState(false);
console.log('App component Init');
return (
<div>
<h2>Let's get started! </h2>
<button
onClick={() => {
setCheck(true);
}}
>
ClickMe
</button>
</div>
);
}
export default App;
i get one log on app init,
upon the first click (state changes from false to true) i get another log as expected.
But on the second click i also get a log , although the state remains the same.(interstingly the ReactDevTools doesn't produce the highlight effect around the component as when it is rerendered)
For every following clicks no log is displayed.
Why is this extra log happening.
Here is a stackblitz demo:
https://stackblitz.com/edit/react-ts-wvaymj?file=index.tsx
Thanks in advance
Given
i get one log on app init,
upon the first click (state changes from false to true) i get another
log as expected.
But on the second click i also get a log , although the state remains
the same.(interstingly the ReactDevTools doesn't produce the highlight
effect around the component as when it is rerendered)
For every following clicks no log is displayed.
And your question being
Why is this extra log happening?
Check the useState Bailing Out of a State Update section (emphasis mine):
If you update a State Hook to the same value as the current state,
React will bail out without rendering the children or firing effects.
(React uses the Object.is comparison algorithm.)
Note that React may still need to render that specific component again
before bailing out. That shouldn’t be a concern because React won’t
unnecessarily go “deeper” into the tree. If you’re doing expensive
calculations while rendering, you can optimize them with useMemo.
The answer to your question is essentially, "It's just the way React and the useState hook work." I'm guessing the second additional render is to check that no children components need to be updated, and once confirmed, all further state updates of the same value are ignored.
If you console log check you can see...
The first click you will get check is true -> check state change from false (init) => true (click) => state change => view change => log expected.
The second click => check state is true (from frist click) => true => state not change => view not render.
So. you can try
setCheck(!check);

React onClick event trigger infinitely on render

So i ran into some funny bug with React onClick event. So if I write something like this:
login = () => {
this.setState({ authed: true })
alert(this.state.authed)
}
render() {
return (
<div>
<Loginpage />
<button onClick={this.login}>test</button>
</div>
);
it would function normally but if onClick is changed to onClick={this.login()}, this event will be triggered on render and continue infinitely even after changing the code back and re-render this will still go on. I'm just curious why it ends up like that, does anyone knows?
If you use this onClick={this.login()} you are calling the function as soon as it renders and not onClick, that's why your component is infinitelly rendering.
So if you do this:
<button onClick={this.login}>test</button>
You are passing a reference to the function login on click of the button.
But if you do this:
<button onClick={this.login()}>test</button>
Then you are executing the function as soon as this element is rendered.
The login function triggers a state change:
login = () => {
this.setState({ authed: true })
alert(this.state.authed)
}
And since state change triggers another render, therefore it goes into the infinite loop.
Had you not called the setState in login, it would not go into the infinite loop.
Only your function will execute without even clicking the button on render.
Try this
<button onClick={()=>this.login}>test</button>

Open Modal on page load causing Maximum update depth exceeded

I'm trying to open a modal on page load but I get 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 know why this is happening because there are many posts about it. I don't know how to fix it though.
I'm using the Material UI modal
I've tried loading it in componentDidMount- caused error. I tried also to use onClick and simulate a click to open it, but this did not work - could not get simulation to work. Below is my latest attempt - caused error.
<Button onLoad={this.openModal.bind(this)()}>Open Modal</Button>
openModal(e) {
this.setState({
open:true
})
}
I can't seem to get past the error and open the modal on load.
In the Modal file, the modal itself looks like this:
<Button onLoad={this.openModal.bind(this)()}>Open Modal</Button>
<Modal
aria-labelledby="simple-modal-title"
aria-describedby="simple-modal-description"
open={this.state.open}
onClose={this.handleClose}
>
In the parent component
<Modal open={this.state.modalState}/> //=> true
I tried this as well (with the event handler removed and not being called this.openModal.bind(this)()). Got the error
componentDidUpdate(){
this.setState({
open:this.props.open
})
}
Generally, for rendering a modal we will use a state to specify is its open or close, If you want to open the modal as soon as the component loads then you can specify the state open as true by default
for example
Class Example extends React.component {
construtor(props) {
super(props)
this.state = { open: true }
}
render() {
return <Modal open={this.state.open}>{//body goes here}</Modal>
}
}
This opens modal by default and you can toggle it depending on your requirement
You are calling setstate in componentdidupdate with out any conditions this will cause stacklevel too deep error as after setstate the componentwillupdate will be invoked again
If you want you can use componentdidmount instead

Relations setState and child component

I made some app to get asynchronously data from remote server.
The app just parent component - to get data, and two child components.
One child component for display asynchronous data.
Other child for filter functionality. Just input string where user typing and data in first component display appropriate items.
There are a lot code with console.log everywhere, but in simple scheme it:
class App extends Component {
state = {isLoading:true, query:''}
getData = (location) => {
axios.get(endPoint).then(response=>{ response.map((item) => { places.push(item)})
// ***** first setState
this.setState({isLoading:false})
})
}
updateQuery = (e) => {
// ***** second setState
this.setState({query:e.target.value.trim()})
}
componentDidMount(){
this.getData(location)
}
render() {
if (!this.state.isLoading){
if (this.state.query){
const match = new RegExp(escapeRegExp(this.state.query),'i')
searchTitles = places.filter(function(item){return match.test(item.name)})
}else{
searchTitles = places.slice();
}
}
return (
<div className="App">
<input type='text' onChange={this.updateQuery} value={this.state.query}/>
<List places = {searchTitles}/>
</div>
);
}
}
export default App;
When state change in case of using everything is OK - content refreshed in next child component.
But child component that display data - some items not full of content... no photos and some text information. So probably its rendered before getting remote data.
But why its not re-render it after state.isLoad toggled to 'false' (in code - after got response) ?
I put among code console.log to track processes ... and weird things: state.isLoad switched to false before some part of data came from server. (((
I dont use ShouldComponentUpdate() inside child component.
Per React's documentation for setState
setState() will always lead to a re-render unless
shouldComponentUpdate() returns false.
As mentioned, one way to avoid a re-render is shouldComponentUpdate returning false (shouldComponentUpdate takes in nextProps and nextState) but it's not clear why someone would trigger a state change with setState and then nullify that state change with shouldComponentUpdate.

Refreshing Component after Route Change

I have a row of buttons that all links to a chart being rendered, then the button pressed, it decides which data will be shown on the chart below.
<div>
<Route path="/" component={Main} />
<Route path="/chart/:topic" component={Chart} />
</div>
Button element:
<Link to={"/chart/" + collection.name}>
<Button key={index} data-key={index} style={this.btnStyle}>
{this.store.capitalizeFirstLetter(collection.name)}
</Button>
</Link>
This works fine when the button is pressed for the first time. However if the user tries to change the data by pressing a different button the chart component does not refresh at all, browser shows that the URL has changed however the component does not refresh at all.
I know this is because of, I've put a console.log in the chart component and it does not come up the second time a button is pressed.
componentDidMount = () => {
const { match: { params } } = this.props;
this.topic = params.topic;
console.log("chart topic", this.topic);
this.refreshData(true);
this.forceUpdate();
this.store.nytTest(this.topic, this.startDate, this.endDate);
};
As you can see I tried to put a forceUpdate() call but that did nothing. Any help is appreciated!
It's because your component is already rendered and didn't see any change so it don't rerender.
You have to use the componentWillReceiveProps method to force the refresh of your component
Example
componentWillReceiveProps(nextProps){
if(nextProps.match.params.topic){
//Changing the state will trigger the rendering
//Or call your service who's refreshing your data
this.setState({
topic:nextProps.match.params.topic
})
}
}
EDIT
The componentWillReceiveProps method is deprecated.
Now the static getDerivedStateFromProps is prefered when you're source data are coming from a props params
Documentation
This method shoud return the new state for trigger the remounting, or null for no refresh.
Example
static getDerivedStateFromProps(props, state) {
if (props.match.params.topic && props.match.params.topic !== state.topic) {
//Resetting the state
//Clear your old data based on the old topic, update the current topic
return {
data: null,
topic: props.match.params.topic
};
}
return null;
}
componentDidMount is only called once while mounting(rendering) the component.
You should use getDerivedStateFromProps to update your component

Resources