Canceling mobx autorun function on componentWillUnmount - javascript

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() {
// ...
}
}

Related

Can I fetch api every time that props change on ComponentDidUpdate?

As you see my code, I want to fetch API contentUrl every its change from props.
but browser throw error like this. Have someone help me?.
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.
Issue is clearly states that : Can't perform a React state update on
an unmounted component
So check if the component is unmounted before setting up state, _isMounted
Code Snippet Ref from : HERE , Hope this will clear all your doubts
class News extends Component {
_isMounted = false; // <----- HERE
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true; // <----- HERE
axios
.get('YOUR_URL')
.then(result => {
if (this._isMounted) { // <----- CHECK HERE BEFORE SETTING UP STATE
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false; // <----- HERE
}
render() {
...
}
}
For example, if a call is done and your component is unmounted, a setState will be called.
You can prevent this with a condition:
_isMount = true;
componentDidUpdate() {
this.props.getContentJSON(url).then(() => {
if(this._isMount){
this.setState({...})
}
})
}
componentWillUnmount() {
this._isMount = false;
}
or control your call :
controller = new AbortController();
componentDidUpdate() {
// I use fetch here but you can adapte to your code
fetch(url, { signal: controller.signal }).then(() => {
this.setState({...})
})
}
componentWillUnmount() {
controller.abort();
}

Updating the react state/component correctly?

I am trying to make my component reactive on updates. I am using componentDidUpdate() to check if the components prop state has changed, then if it has it is has I need the getPosts() function to be called and the postCount to update if that prop is changed.
export default class JsonFeed extends React.Component<IJsonFeedProps, IJsonFeedState> {
// Props & state needed for the component
constructor(props) {
super(props);
this.state = {
description: this.props.description,
posts: [],
isLoading: true,
jsonUrl: this.props.jsonUrl,
postCount: this.props.postCount,
errors: null,
error: null
};
}
// This function runs when a prop choice has been updated
componentDidUpdate() {
// Typical usage (don't forget to compare props):
if (this.state !== this.state) {
this.getPosts();
// something else ????
}
}
// This function runs when component is first renderd
public componentDidMount() {
this.getPosts();
}
// Grabs the posts from the json url
public getPosts() {
axios
.get("https://cors-anywhere.herokuapp.com/" + this.props.jsonUrl)
.then(response =>
response.data.map(post => ({
id: `${post.Id}`,
name: `${post.Name}`,
summary: `${post.Summary}`,
url: `${post.AbsoluteUrl}`
}))
)
.then(posts => {
this.setState({
posts,
isLoading: false
});
})
// We can still use the `.catch()` method since axios is promise-based
.catch(error => this.setState({ error, isLoading: false }));
}
You can change componentDidUpdate to:
componentDidUpdate() {
if (this.state.loading) {
this.getPosts();
}
}
This won't be an infinite loop as the getPosts() function sets state loading to false;
Now every time you need an update you just need to set your state loading to true.
If what you want to do is load everytime the jsonUrl updates then you need something like:
componentDidUpdate(prevProps) {
if (prevProps.jsonUrl!== this.props.jsonUrl) {
this.getPosts();
}
}
Also I don't get why you expose your components state by making componentDidMount public.
Modify your getPosts to receive the jsonUrl argument and add the following function to your class:
static getDerivedStateFromProps(props, state) {
if(props.jsonUrl!==state.jsonUrl){
//pass jsonUrl to getPosts
this.getPosts(props.jsonUrl);
return {
...state,
jsonUrl:props.jsonUrl
}
}
return null;
}
You can get rid of the componentDidUpdate function.
You can also remove the getPosts from didmount if you don't set state jsonUrl in the constructor.
// This function runs when a prop choice has been updated
componentDidUpdate(prevProps,prevState) {
// Typical usage (don't forget to compare props):
if (prevState.jsonUrl !== this.state.jsonUrl) {
this.getPosts();
// something else ????
}
}
this way you have to match with the updated state
Try doing this
componentDidUpdate(prevState){
if(prevState.loading!==this.state.loading){
//do Something
this.getPosts();
}}

componentWillUnmount with React useEffect hook

How can the useEffect hook (or any other hook for that matter) be used to replicate componentWillUnmount?
In a traditional class component I would do something like this:
class Effect extends React.PureComponent {
componentDidMount() { console.log("MOUNT", this.props); }
componentWillUnmount() { console.log("UNMOUNT", this.props); }
render() { return null; }
}
With the useEffect hook:
function Effect(props) {
React.useEffect(() => {
console.log("MOUNT", props);
return () => console.log("UNMOUNT", props)
}, []);
return null;
}
(Full example: https://codesandbox.io/s/2oo7zqzx1n)
This does not work, since the "cleanup" function returned in useEffect captures the props as they were during mount and not state of the props during unmount.
How could I get the latest version of the props in useEffect clean up without running the function body (or cleanup) on every prop change?
A similar question does not address the part of having access to the latest props.
The react docs state:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
In this case however I depend on the props... but only for the cleanup part...
You can make use of useRef and store the props to be used within a closure such as render useEffect return callback method
function Home(props) {
const val = React.useRef();
React.useEffect(
() => {
val.current = props;
},
[props]
);
React.useEffect(() => {
return () => {
console.log(props, val.current);
};
}, []);
return <div>Home</div>;
}
DEMO
However a better way is to pass on the second argument to useEffect so that the cleanup and initialisation happens on any change of desired props
React.useEffect(() => {
return () => {
console.log(props.current);
};
}, [props.current]);
useLayoutEffect() is your answer in 2021
useLayoutEffect(() => {
return () => {
// Your code here.
}
}, [])
This is equivalent to ComponentWillUnmount.
99% of the time you want to use useEffect, but if you want to perform any actions before unmounting the DOM then you can use the code I provided.
useLayoutEffect is great for cleaning eventListeners on DOM nodes.
Otherwise, with regular useEffect ref.current will be null on time hook triggered
More on react docs https://reactjs.org/docs/hooks-reference.html#uselayouteffect
import React, { useLayoutEffect, useRef } from 'react';
const audioRef = useRef(null);
useLayoutEffect(() => {
if (!audioRef.current) return;
const progressEvent = (e) => {
setProgress(audioRef.current.currentTime);
};
audioRef.current.addEventListener('timeupdate', progressEvent);
return () => {
try {
audioRef.current.removeEventListener('timeupdate', progressEvent);
} catch (e) {
console.warn('could not removeEventListener on timeupdate');
}
};
}, [audioRef.current]);
Attach ref to component DOM node
<audio ref={audioRef} />
useEffect(() => {
if (elements) {
const cardNumberElement =
elements.getElement('cardNumber') || // check if we already created an element
elements.create('cardNumber', defaultInputStyles); // create if we did not
cardNumberElement.mount('#numberInput');
}
}, [elements]);

React-Navigation: Call function whenever page is navigated to

I am developing a React-Native app using React-Navigation, and I am using a stack navigator.
How can I call a function whenever a page is navigated to, including on goBack() events? If I place the method in my constructor, it is only triggered on its initial creation, and not when it is attained through goBack().
use Navigation Events
I believe you can use did focus and will blur
<NavigationEvents
onWillFocus={payload => console.log('will focus', payload)}
onDidFocus={payload => console.log('did focus', payload)}
onWillBlur={payload => console.log('will blur', payload)}
onDidBlur={payload => console.log('did blur', payload)}
/>
https://reactnavigation.org/docs/en/navigation-events.html
EDIT 2022
import { useIsFocused } from '#react-navigation/native';
const isFocused = useIsFocused();
React.useEffect(()=>{
if(isFocused){
// callback
}
},[isFocused])
As you noted the component is never unmounted when changing pages, so you can't rely on the constructor or even componentDidMount. There is a lot of discussion about this topic in this issue.
You could e.g. listen to the didFocus and willBlur events and only render your page when it is focused.
Example
class MyPage extends React.Component {
state = {
isFocused: false
};
componentDidMount() {
this.subs = [
this.props.navigation.addListener("didFocus", () => {
this.setState({ isFocused: true })
}),
this.props.navigation.addListener("willBlur", () => {
this.setState({ isFocused: false })
})
];
}
componentWillUnmount() {
this.subs.forEach(sub => sub.remove());
}
render() {
const { isFocused } = this.state;
if (!isFocused) {
return null;
}
return <MyComponent />;
}
}

Calling setState on an unmounted component

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;
}

Categories