'How do I prevent methods in an Unmounted class component from being called inadvertently?
I'm working on a React-Redux app which allows Google oauth2 login, and I've created a separate GoogleAuth component that handles all the login/logout processes.
The GoogleAuth component is usually in the header, as a child of the Header component. However, there are some pages (shown when the user is not logged in) in which I place the GoogleAuth component elsewhere, and remove it from the header.
For example, on the Login page, the GoogleAuth component is unmounted from the header, and another instance is mounted within the Login page.
I've included the GoogleAuth component's code below.
class GoogleAuth extends React.Component {
componentDidMount() {
window.gapi.load('client:auth2', () => {
window.gapi.client.init({
clientId: 'xxxx',
scope: 'email'
}).then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
const isGoogleSignedIn = this.auth.isSignedIn.get();
if(this.props.isSignedIn !== isGoogleSignedIn) {
this.onAuthChange(isGoogleSignedIn);
}
this.auth.isSignedIn.listen(this.onAuthChange);
})
});
};
componentWillUnmount() {
delete this.auth;
}
onAuthChange = (isGoogleSignedIn) => {
console.log("onAuthChange called:", this.props.place);
//place is a label assigned to the parent component
if (!this.props.isSignedIn && isGoogleSignedIn && this.auth){
this.props.signIn(this.auth.currentUser.get().getId());
} else if (this.props.isSignedIn && !isGoogleSignedIn && this.auth) {
this.props.signOut();
}
}
handleSignIn = () => {
this.auth.signIn();
}
handleSignOut = () => {
this.auth.signOut();
}
renderAuthButton() {
if (this.props.isSignedIn === null) {
return null;
} else if (this.props.isSignedIn) {
return (
<button className="google-auth-button" onClick={this.handleSignOut}>
Sign Out
</button>
)
} else {
return (
<button className="google-auth-button" onClick={this.handleSignIn}>
Sign In With Your Google Account
</button>
)
}
}
render() {
return this.renderAuthButton()
}
}
const mapStateToProps = (state) => {
return {
isSignedIn: state.auth.isSignedIn,
userId: state.auth.userId
}
}
export default connect(
mapStateToProps,
{signIn, signOut}
)(GoogleAuth);
The issue I'm facing is that the onAuthChange method (triggered when the user's auth status changes) is being called once for every time the GoogleAuth component was ever mounted, even if it has been unmounted since. So if I open the app and then go to the Login page (which results in GoogleAuth being unmounted from the Header and inserted into the Login component), and then sign in, I see the following console logs.
onAuthChange called: header
onAuthChange called: login
I have a few things I'd appreciate some advice on:
- Is this an issue that I need to resolve? What would happen if I did not do anything to resolve it?
- I tried a little clean up when the component unmounts, by deleting the
authobject explicitly. However, theonAuthChangemethod was still called for every instance of theGoogleAuthcomponent whether mounted or not. - I was able to prevent duplicate calls to my action creators by checking whether
this.authexists. Any drawbacks to this approach? - Is there a time after which the unmounted components will get automatically cleared?
Solution 1:[1]
- Yes it is, as it's basically a memory leak. The logs tell you that the function which calls
console.logis not being GCed - The
this.authis set towindow.gapi.auth2.getAuthInstance().delete this.authonly removes theauthkey from yourthis. As long as there are other references to the object returned bywindow.gapi.auth2.getAuthInstance()(for example in the library itself, looks a lot like a singleton) it will not be GCed -> yourdeletedoesn't make much of a difference - See point 1.
- Yes, when the user closes your tab.
To resolve the issue you'll have to remove the listener from this.auth.isSignedIn in your componentWillUnmount, possibly there's a function called this.auth.isSignedIn.removeListener(...). If not you can check the documentation on how to remove the listener.
I'd also advise against loading the API and initialising your client in every instance of your component and much rather do it only a single time.
Edit: To unregister the listener you can have a look at this question: How to remove Google OAuth2 gapi event listener? However I'd advise against taking the route of adding/removing listeners for every component instance.
Edit 2: Example
class GoogleAuth extends React.Component {
componentDidMount() {
googleAuthPromise.then(
googleAuth => {
const isGoogleSignedIn = googleAuth.isSignedIn.get();
if(this.props.isSignedIn !== isGoogleSignedIn) {
this.onAuthChange(isGoogleSignedIn);
}
this.unregisterListener = listenSignIn(this.onAuthChange);
}
)
};
componentWillUnmount() {
this.unregisterListener();
}
onAuthChange = async (isGoogleSignedIn) => {
console.log("onAuthChange called:", this.props.place);
const googleAuth = await googleAuthPromise;
if (!this.props.isSignedIn && isGoogleSignedIn && googleAuth){
this.props.signIn(googleAuth.currentUser.get().getId());
} else if (this.props.isSignedIn && !isGoogleSignedIn && googleAuth) {
this.props.signOut();
}
}
handleSignIn = async () => {
const googleAuth = await googleAuthPromise;
return googleAuth.signIn();
}
handleSignOut = async () => {
const googleAuth = await googleAuthPromise;
return googleAuth.signOut();
}
renderAuthButton() {
if (this.props.isSignedIn === null) {
return null;
} else if (this.props.isSignedIn) {
return (
<button className="google-auth-button" onClick={this.handleSignOut}>
Sign Out
</button>
)
} else {
return (
<button className="google-auth-button" onClick={this.handleSignIn}>
Sign In With Your Google Account
</button>
)
}
}
render() {
return this.renderAuthButton()
}
}
const mapStateToProps = (state) => {
return {
isSignedIn: state.auth.isSignedIn,
userId: state.auth.userId
}
}
export default connect(
mapStateToProps,
{signIn, signOut}
)(GoogleAuth);
// create a customer listener manager in order to be able to unregister listeners
let listeners = []
const invokeListeners = (...args) => {
for (const listener of listeners) {
listener(...args)
}
}
const listenSignIn = (listener) => {
listeners = listeners.concat(listener)
return () => {
listeners = listeners.filter(l => l !== listener)
}
}
// make the client a promise to be able to wait for it
const googleAuthPromise = new Promise((resolve, reject) => {
window.gapi.load('client:auth2', () => {
window.gapi.client.init({
clientId: 'xxxx',
scope: 'email'
}).then(resolve, reject)
})
})
// register our custom listener handler
googleAuthPromise.then(googleAuth => {
googleAuth.isSignedIn.listen(invokeListeners)
})
Disclaimer: this is untested code, you might need to make small corrections.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
