Warning: 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.
Why am I still getting an error about setting state in an unmounted component? In the error trace it points to the setTimeout in foo(). I clear my async timer and also add a check before I perform my api call - I don't see where this warning is coming from.
componentDidMount() {
this.setState({alive: true});
this.foo();
}
componentWillUnmount() {
this.setState({alive: false, timer: 0});
}
foo() {
if (!this.state.alive) return;
fetch('/api/etc/', {method: 'GET', headers: {'Cache-Control': 'no-cache'}})
.then(res => res.json())
.then(json => {
if (!json.length) return;
this.setState((prevState) => ({
timer: setTimeout(this.foo.bind(this), 500)
});
});
}
* Updated Answer & also Function Component version with React Hooks *
As Evan Trimboli pointed out in the comment below, there is no need to store the timeout ID in the state as it doesn't impact rendering.
So store the timeout ID in the class instance and use it to clear timeout in componentWillUnmount.
Run the code below to see it in action
Class Component version (prior to, not including v16.8.0)
class TodoApp extends React.Component {
timeout = 0;
hello = () => console.log("hello world!")
componentDidMount() {
this.timeout = setTimeout(this.hello, 500);
}
componentWillUnmount() {
clearTimeout(this.timeout);
}
render() {
return (
<div>demo</div>
)
}
}
ReactDOM.render(<TodoApp />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
Function Component version with a Hook (possible since v16.8.0)
React.useEffect let's you put a setup & teardown logic in a same block.
const TodoApp = () => {
const hello = () => console.log("hello world!")
React.useEffect(() => {
const timeout = setTimeout(hello, 500);
return () => clearTimeout(timeout);
});
return (
<div>demo</div>
)
}
ReactDOM.render(<TodoApp />, document.querySelector("#app"))
<script src="https://unpkg.com/react#16.8.4/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Old Answer (using State to store timeout ID).
Store the timeout ID in the state, and use that to clear out the timeout in componentWillUnmount.
class TodoApp extends React.Component {
constructor(props) {
super(props)
this.state = {
timeout: null
}
this.hello = this.hello.bind(this);
}
hello() { console.log("hello world!"); }
componentDidMount() {
const timeout = setTimeout(this.hello, 500);
this.setState({timeout});
}
componentWillUnmount() {
clearTimeout(this.state.timeout);
}
render() {
return (
<div>demo</div>
)
}
}
ReactDOM.render(<TodoApp />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
Related
I have some event listeners on my props
constructor(props) {
super(props);
Tts.addEventListener("tts-start", event =>
console.log("started"),
this.setState({ ttsStatus: "started" })
//how to call stopTTS
); ...}
so if I have a function outside the constructor
stopTTS() {
console.log("cali");
}
how to call a function when the eventListener gets triggered? cheers
First: If you can, use hooks instead.
A functional component that can do what you want could be:
import React, { useEffect, useState } from 'react'
const Component = () => {
const [ttsStatus, setTtsStatus] = useState('')
const stopTTS = () => {
console.log("cali");
}
// This useEffect will work as a componentDidMount
useEffect(() => {
Tts.addEventListener("tts-start", event => {
console.log("started"),
setTtsStatus("started")
stopTTS() // You can call stopTTS here
})
}, [])
return null
}
export default Component
Try to avoid creating classes, the React Hooks were a new addition in React 16.8. They let you use state and other React features without writing a class, so you can have the power of a class in a cleaner function. You can know more about it in https://reactjs.org/docs/hooks-overview.html
As i mentioned in the comment, you can call the class methods inside constructor like below snippet.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
window.addEventListener('click', (e) => {
if (this.state.count > 4) {
this.alertCount();
}
});
}
alertCount = () => {
alert('count has become 5')
this.setState({
count: 0
})
}
clickHandler = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div >
<div>
The count is now {this.state.count}
</div>
<button onClick = {
this.clickHandler
} id = "btn" > Click Me < /button>
</div >
);
}
}
ReactDOM.render( < App / > , document.getElementById('root'))
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Having the below jsx code:
import React, { Component } from 'react';
import RemoteAssets from '../modules/RemoteAssets';
class RemoteOptions extends Component {
constructor(props) {
super(props);
this.state = {
RemoteOptions: []
}
}
componentDidMount() {
const { api, locale } = this.props;
RemoteAssets.loadRemoteOptions({ api, locale }).then((RemoteOptions) => {
console.log( 'RemoteOptions', RemoteOptions);
this.setState((state, props) => ({
RemoteOptions
}), () => {
this.render()
});
})
}
render() {
return (
<div className="row">
<div className="col-4">
<label >Opt: </label>
</div>
<div className=" col-8">
{JSON.stringify(this.state.RemoteOptions)}
</div>
</div>
);
}
}
export default RemoteOptions;
This is what happens to me:
componentDidMount logs correctly the payload expected.
console.log( 'RemoteOptions', RemoteOptions);
So I believe that It will also set State as expected:
this.setState((state, props) => ({
RemoteOptions
}), () => {
this.render()
});
I also added above a this.render() stmt to be sure the component will be re-rendered after updating the state.
But :
{JSON.stringify(this.state.RemoteOptions)}
Will always return "[]" as the init state before componentDidMount happens and update the state.
How should I arrange this component to have my render update the with the payĆ²oad loaded async?
Name conflict
Your state name and class name are in conflict.
class RemoteOptions extends Component { // class name
constructor(props) {
super(props);
this.state = {
RemoteOptions: [] // state name
}
}
...
Call your state something different.
Why not simply using setState the way documentation suggests?
this.setState({ RemoteOptions });
Render method will be automatically called right after the state is set.
I implemented the skeleton of the problem, and everything works as expected.
const loadRemoteOptions = () => new Promise(resolve => {
setTimeout(() => resolve('myRemoteOptions'), 1000)
})
class App extends React.Component {
state = {
remoteOptions: null
}
componentDidMount(){
loadRemoteOptions().then(remoteOptions => this.setState({ remoteOptions }))
}
render(){
return this.state.remoteOptions || 'Loading...';
}
}
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In a lot of my components I need to do something like this:
handleSubmit() {
this.setState({loading: true})
someAsyncFunc()
.then(() => {
return this.props.onSuccess()
})
.finally(() => this.setState({loading: false}))
}
The onSuccess function
may or may not be a promise (if it is, loading should stay true until it is resolved)
may or may not unmount the component (it may close the modal this component is in or even navigate to different page)
If the function unmounts the component, this.setState({loading: false}) obviously triggers a warning Can't call setState (or forceUpdate) on an unmounted component.
My 2 questions:
Is there a simple way to avoid the issue ? I don't want to set some _isMounted variable in componentDidMount and componentWillUnmount and then check it when needed in most of my components, plus I may forget to do it next time writing something like this ...
Is it really a problem ? I know that, according to the warning, it indicates a memory leak in my application, but it is not a memory leak in this case, is it ? Maybe ignoring the warning would be ok ...
EDIT: The second question is a little bit more important for me than the first. If this really is a problem and I just can't call setState on unmounted component, I'd probably find some workaround myself. But I am curious if I can't just ignore it.
Live example of the problem:
const someAsyncFunc = () => new Promise(resolve => {
setTimeout(() => {
console.log("someAsyncFunc resolving");
resolve("done");
}, 2000);
});
class Example extends React.Component {
constructor(...args) {
super(...args);
this.state = {loading: false};
}
componentDidMount() {
setTimeout(() => this.handleSubmit(), 100);
}
handleSubmit() {
this.setState({loading: true})
someAsyncFunc()
/*
.then(() => {
return this.props.onSuccess()
})
*/
.finally(() => this.setState({loading: false}))
}
render() {
return <div>{String(this.state.loading)}</div>;
}
}
class Wrapper extends React.Component {
constructor(props, ...rest) {
super(props, ...rest);
this.state = {
children: props.children
};
}
componentDidMount() {
setTimeout(() => {
console.log("removing");
this.setState({children: []});
}, 1500)
}
render() {
return <div>{this.state.children}</div>;
}
}
ReactDOM.render(
<Wrapper>
<Example />
</Wrapper>,
document.getElementById("root")
);
.as-console-wrapper {
max-height: 100% !important;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
Unfortunately you have to keep track of "isMounted" yourself.
To simplify you control flow you could use async/await:
handleSubmit() {
this.setState({loading: true})
try {
await someAsyncFunction()
await this.props.onSuccess()
} finally {
if (this._isMounted) {
this.setState({loading: false})
}
}
}
This is actually mentioned in the react docs, which points to this solution: https://gist.github.com/bvaughn/982ab689a41097237f6e9860db7ca8d6
If your someAsyncFunction supports cancelation, you should do so in componentWillUnmount, as encouraged by this article. But then - of course - check the return value and eventually not call this.props.onSuccess.
class myClass extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentDidMount() {
this._isMounted = true;
this._getData();
}
componentWillUnmount() {
this._isMounted = false;
}
_getData() {
axios.get('example.com').then(data => {
if (this._isMounted) {
this.setState({ data })
}
});
}
render() {
...
}
}
You should be able to use this._isMounted to check if the component is actually mounted.
handleSubmit() {
this.setState({loading: true})
someAsyncFunc()
.then(() => {
return this.props.onSuccess()
})
.finally(() => {
if (this && this._isMounted) { // check if component is still mounted
this.setState({loading: false})
}
})
}
But be aware that this approach is considered to be an anitpattern. https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
What about
componentWillUnmount() {
// Assign this.setState to empty function to avoid console warning
// when this.setState is called on an unmounted component
this.setState = () => undefined;
}
I have the following autorun function in my componentDidMount:
componentDidMount() {
this.autoUpdate = autorun(() => {
this.setState({
rows: generateRows(this.props.data)
})
})
}
The problem is that another component changes this.props.data when the component is not mount - and therefor I get the .setState warning on an unmounted component.
So I would like to remove the autorun once the component unmounts.
I tried doing:
componentWillUnmount() {
this.autoUpdate = null
}
But the autorun function still triggers. Is there a way to cancel the mobx autorun once the component is not mounted any more?
autorun returns a disposer function that you need to call in order to cancel it.
class ExampleComponent extends Component {
componentDidMount() {
this.autoUpdateDisposer = autorun(() => {
this.setState({
rows: generateRows(this.props.data)
});
});
}
componentWillUnmount() {
this.autoUpdateDisposer();
}
render() {
// ...
}
}
Let's say I have a Timestamp component that should update every 6000 milliseconds. Here is the base:
class Timestamp extends Component {
static propTypes = {
timestamp: PropTypes.date.isRequired
};
render() {
const { timestamp } = this.props;
return (
<div className="timestamp">
{moment(timestamp).fromNow()}
</div>
)
}
}
I have read through the lifecycle of a react component, and it looks like shouldComponentUpdate is what I want -- however, there does not seem to be an asynchronous way to apply this. Eg:
shouldComponentUpdate() {
const { timestamp } = this.props;
this._timer = setInterval(() => {
// need to update
})
}
How should this be accomplished in react?
In this case better start timer in componentDidMount, and in setInterval call setState which triggers re-render
class Timestamp extends React.Component {
constructor() {
super();
this._timer = null;
this.state = { timestamp: Date.now() };
}
componentDidMount() {
this._timer = setInterval(() => this.onChangeTimestamp(), 6000);
}
componentWillUnmount() {
clearInterval(this._timer);
}
onChangeTimestamp() {
this.setState({ timestamp: Date.now() })
}
render() {
return (
<div className="timestamp">
{ new Date(this.state.timestamp).toString() }
</div>
)
}
}
ReactDOM.render(
<Timestamp />,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
Considering only React, updating your component can be done either using setState() or using forceUpdate (that you should avoid).
In both cases, this is not in shouldComponentUpdate that this should be done. Put setInterval(() in componentDidMount(){}