18

I have added the listener in the following way(tried putting in both constructor and componentDidMount): AppState.addEventListener('change', this._handleAppStateChange);

And the removed the listener the following way in componentWillUnmount method:

AppState.removeEventListener('change', this._handleAppStateChange);

And in the callback function:

  _handleAppStateChange = (nextAppState) => {
    setTimeout(() => {
      alert('App state: ' + this.state.appState);
      alert('Next App state: ' + nextAppState);
    }, 0);
  }

It alerts several times. Its not removing the listener configured once. Please let me know if someone is aware of it ?

gbhati
  • 493
  • 1
  • 8
  • 20
  • I'm also hitting an issue with multiple listeners, but it only happens when I launch the app multiple times – toppsdown Jun 07 '18 at 01:25

8 Answers8

32

According to the latest docs (September 2021 v0.65+) removeEventListener is deprecated

The docs now recommend using the remove function on the subscription object (EmitterSubscription) which is returned from AppState.addEventListener.

Example usage:

const subscription = AppState.addEventListener('change', (appState) => {
  if (appState !== 'active') {
    return;
  }

  // Run custom logic

  subscription.remove();
});
John Kalberer
  • 5,690
  • 1
  • 23
  • 27
  • Thanks for the update! However I believe there is a typo on the last sentence, `AppState.removeEventListener` should be `AppState.addEventListener` instead. – Jarrett Sep 28 '21 at 03:29
  • 3
    However, it seems that `AppState.addEventListener` doesn't actually return the `remove` method. The example given in the docs doesn't work too. – Jarrett Sep 28 '21 at 03:33
  • Are you on v0.65+? If not this won't work for you. Also, if you are running this inside a component there is a possibility that you are adding the event listener multiple times. @PoulsQ's answer would probably help you. – John Kalberer Sep 30 '21 at 15:33
  • 2
    I am using Expo, so I guess they have not added support for the latest version of react native. The old method of using `.removeEventListener` still works however. Thanks anyway! – Jarrett Oct 01 '21 at 05:30
  • I have a typescript error with `remove()` method: `Property 'remove' does not exist on type 'void'. ts(2339)` – Arthur Nov 24 '21 at 16:20
  • Thanks for sharing, shouldn't `subscription.remove();` be out of the function scope? – Rafa2602 Nov 25 '21 at 11:24
  • 1
    @Arthur - are you sure you have the latest typescript definitions for v0.65+? Another issue could be that the typescript definitions have not been updated. – John Kalberer Nov 25 '21 at 17:25
  • @Rafa2602 - no, in my case it was intentional because I wanted to unsubscribe after the event listener ran. In your case you might want to call it when the component unmounts. – John Kalberer Nov 25 '21 at 17:26
16

You should use API like now on

useEffect(() => {
 const myListener = AppState.addEventListener('change', this.someHandler)
 return () => {
   myListener.remove()
 }
}, [])
Charney Kaye
  • 3,667
  • 6
  • 41
  • 54
Yadigar ZENGİN
  • 189
  • 1
  • 4
4

Faced the same issue these last days. I've finally managed it by deporting my app state management to my App.js component and created a service manager.

Here quickly how my App.js look like:

import {AppState } from "react-native";
import {AppStateService} from "YOUR_PATH_TO_THE_NEXT_FILE";

export default function App() {

    // Listen to app state
    AppStateService.init();
    useEffect(() => {
        AppState.addEventListener('change', AppStateService.getInstance().handleAppStateChange);
        return (() => {
            AppState.removeEventListener('change', AppStateService.getInstance().handleAppStateChange);
        })
    }, []);

    return (/*Rendering stuff (navigation, error boundary, ...*/);
}

AppStateService.js:

/**
 * Class to allow us to refer to the app state service
 */

export class AppStateService {

    static instance;

    static STATE_ACTIVE         = 'active';
    static STATE_INACTIVE       = 'inactive';
    static STATE_BACKGROUND     = 'background';
    static STATE_NOT_LAUNCHED   = 'not_launched';

    previousState   = AppStateService.STATE_NOT_LAUNCHED;
    currentState    = AppStateService.STATE_ACTIVE;

    handlers = {};

    appLaunchId = 0;

    /**
     * @returns {AppStateService}
     */
    static getInstance() {
        if(!this.instance){
            this.instance = new AppStateService();
        }

        return this.instance;
    }

    static init = () => {
        // This func need to be call in the App.js, it's just here to create the instance
        const instance = AppStateService.getInstance();

        instance.appLaunchId = new Date().getTime() / 1000;
    }

    handleAppStateChange = (nextState) => {
        if(nextState !== this.currentState) {
            this.previousState = this.currentState;
            this.currentState = nextState;

            for (const [key, handler] of Object.entries(this.handlers)) {
                handler(nextState);
            }
        }
    }

    getCurrentState = () => {
        return this.currentState;
    }

    getPreviousState = () => {
        return this.previousState;
    }

    addStateHandler = (key, handler) => {
        this.handlers[key] = handler;
    }

    hasStateHandler = (key) => {
        if( this.handlers[key] ){
            return true;
        }

        return false;
    }

    removeStateHandler = (key) => {
        delete this.handlers[key];
    }
}

and now here is how to call it from anywhere you want in your app components:

export default class RandomComponent extends React.Component {
    
    componentDidMount() {
        // Check app going background or not
        this.handleAppStateChange = this.handleAppStateChange.bind(this);
        AppStateService.getInstance().addStateHandler('myListenerCustomKey', this.handleAppStateChange);
    }

    componentWillUnmount() {
        // Remove app state change listener
        AppStateService.getInstance().removeStateHandler('myListenerCustomKey');
    }

    handleAppStateChange = (nextAppState) => {
        console.log("I'm going to be -" + nextAppState + "- while I was -" + AppStateService.getInstance().getPreviousState() + "-");
    }
}

This way you have the capability to listen everywhere in your app to the app foreground/inactive/background state and correctly subscribe/unsubscribe to those events.

PoulsQ
  • 1,936
  • 1
  • 15
  • 22
1

I'm assuming you're using a class component by the looks of your code. The way I ended up solving it was by creating a pointer function that points to the actual function inside the scope of this, without using .bind(this).

E.g.

// Actual Function    
handleAppStateChange(state) {
  // Work your magic!
}

// Pointer
handleAppStateChangeCall = (state) => this.handleAppStateChange(state);

// Setup listener event
setupAppStateListener() {
  AppState.addEventListener("change", this.handleAppStateChangeCall);
}

// Clear listener event
clearAppStateListener() {
  AppState.addEventListener("change", this.handleAppStateChangeCall);
}

// Mounted Hook
componentDidMount() { 
  setupAppStateListener();
}

// Unmount Hook
componentWillUnmount() {
  clearAppStateListener()
}
uloco
  • 2,283
  • 4
  • 22
  • 37
0

You have to remove the listener from ComponentWillUnmount function

componentWillUnmount() {
    AppState.removeEventListener('change', this._handleAppStateChange);
  }
Tarik Fojnica
  • 665
  • 6
  • 15
0

Set state is an Async process. So in componentWillUnmount don't use it as the component get unmount and still for that scene the setState is in process causing the alert.

Pramod
  • 1,835
  • 8
  • 14
0

May be caused by a change in the function you are listening on

this.props.navigation.addListener(
  'didFocus',
  () => {
    AppState.addEventListener('change', this.handleAppStateChange)
  }
)
this.props.navigation.addListener(
  'willBlur',
  () => {
    AppState.removeEventListener('change', this.handleAppStateChange)
  }
)

it work normal

this.props.navigation.addListener(
  'didFocus',
  () => {
    AppState.addEventListener('change', this.handleAppStateChange.bind(this))
  }
)
this.props.navigation.addListener(
  'willBlur',
  () => {
    AppState.removeEventListener('change', this.handleAppStateChange.bind(this))
  }
)

it not work so maybe you need like this

this.handleAppStateChange = this.handleAppStateChange.bind(this)

in your constructor

0

We can change code as follow:

useEffect(() => { const stateListener = AppState.addEventListener('change', handleAppStateChange);

return () => {
  stateListener.remove();
};

}, []);

const handleAppStateChange = (nextAppState) => { if (appState.current.match(/inactive|background/) && nextAppState === 'active') { console.log('App has come to the foreground!'); setConnectStatus(true); } else { console.log('App has come to the background or inactive!'); setConnectStatus(false); }

appState.current = nextAppState;
setAppStateVisible(appState.current);
console.log('AppState is ', appState.current);

};

AngelDev
  • 106
  • 1
  • 4