Skip to content

Commit

Permalink
RN: Simplify AppState
Browse files Browse the repository at this point in the history
Summary:
Simplifies `AppState` by removing redundant methods and changing `addEventListener` to return an `EventSubscription`.

Changelog:
[General][Changed] - `AppState.addEventListener` now returns an `EventSubscription` object.
[General][Removed] - Removed `AppState.removeEventListener`. Instead, use the `remove()` method on the object returned by `AppState.addEventListener`.
[General][Removed] - `AppState` no longer inherits from `NativeEventEmitter`, so it no longer implements `addListener`, `removeAllListeners`, and `removeSubscription`.

Reviewed By: wtfil

Differential Revision: D26161343

fbshipit-source-id: b3cff76bf0f8f7d79cd954fdef551d0654c682ca
  • Loading branch information
yungsters authored and facebook-github-bot committed Feb 4, 2021
1 parent 6774358 commit 6f22989
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 178 deletions.
231 changes: 62 additions & 169 deletions Libraries/AppState/AppState.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @flow
*/

import {type EventSubscription} from '../vendor/emitter/EventEmitter';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import type EventSubscription from '../vendor/emitter/_EventSubscription';
import type EmitterSubscription from '../vendor/emitter/_EmitterSubscription';
import logError from '../Utilities/logError';
import EventEmitter from '../vendor/emitter/EventEmitter';
import NativeAppState from './NativeAppState';
import invariant from 'invariant';

export type AppStateValues = 'inactive' | 'background' | 'active';

Expand All @@ -37,56 +34,48 @@ type NativeAppStateEventDefinitions = {
*
* See https://reactnative.dev/docs/appstate.html
*/
class AppState extends NativeEventEmitter<NativeAppStateEventDefinitions> {
_eventHandlers: {
[key: $Keys<AppStateEventDefinitions>]: Map<
/* Handler */ $FlowFixMe,
EventSubscription<NativeAppStateEventDefinitions, $FlowFixMe>,
>,
...,
};
_supportedEvents: $ReadOnlyArray<$Keys<AppStateEventDefinitions>> = [
'change',
'memoryWarning',
'blur',
'focus',
];
currentState: ?string;
class AppState {
currentState: ?string = null;
isAvailable: boolean;

constructor() {
super(NativeAppState);

this.isAvailable = true;
this._eventHandlers = this._supportedEvents.reduce((handlers, key) => {
handlers[key] = new Map();
return handlers;
}, {});

this.currentState = NativeAppState.getConstants().initialAppState;

let eventUpdated = false;
_emitter: ?NativeEventEmitter<NativeAppStateEventDefinitions>;

// TODO: this is a terrible solution - in order to ensure `currentState`
// prop is up to date, we have to register an observer that updates it
// whenever the state changes, even if nobody cares. We should just
// deprecate the `currentState` property and get rid of this.
this.addListener('appStateDidChange', appStateData => {
eventUpdated = true;
this.currentState = appStateData.app_state;
});

// TODO: see above - this request just populates the value of `currentState`
// when the module is first initialized. Would be better to get rid of the
// prop and expose `getCurrentAppState` method directly.
NativeAppState.getCurrentAppState(appStateData => {
// It's possible that the state will have changed here & listeners need to be notified
if (!eventUpdated && this.currentState !== appStateData.app_state) {
constructor() {
if (NativeAppState == null) {
this.isAvailable = false;
} else {
this.isAvailable = true;

const emitter: NativeEventEmitter<NativeAppStateEventDefinitions> = new NativeEventEmitter(
NativeAppState,
);
this._emitter = emitter;

this.currentState = NativeAppState.getConstants().initialAppState;

let eventUpdated = false;

// TODO: this is a terrible solution - in order to ensure `currentState`
// prop is up to date, we have to register an observer that updates it
// whenever the state changes, even if nobody cares. We should just
// deprecate the `currentState` property and get rid of this.
emitter.addListener('appStateDidChange', appStateData => {
eventUpdated = true;
this.currentState = appStateData.app_state;
// $FlowExpectedError[incompatible-call]
this.emit('appStateDidChange', appStateData);
}
}, logError);
});

// TODO: see above - this request just populates the value of `currentState`
// when the module is first initialized. Would be better to get rid of the
// prop and expose `getCurrentAppState` method directly.
// $FlowExpectedError[incompatible-call]
NativeAppState.getCurrentAppState(appStateData => {
// It's possible that the state will have changed here & listeners need to be notified
if (!eventUpdated && this.currentState !== appStateData.app_state) {
this.currentState = appStateData.app_state;
emitter.emit('appStateDidChange', appStateData);
}
}, logError);
}
}

// TODO: now that AppState is a subclass of NativeEventEmitter, we could
Expand All @@ -103,133 +92,37 @@ class AppState extends NativeEventEmitter<NativeAppStateEventDefinitions> {
addEventListener<K: $Keys<AppStateEventDefinitions>>(
type: K,
handler: (...$ElementType<AppStateEventDefinitions, K>) => void,
): void {
invariant(
this._supportedEvents.indexOf(type) !== -1,
'Trying to subscribe to unknown event: "%s"',
type,
);

): EventSubscription {
const emitter = this._emitter;
if (emitter == null) {
throw new Error('Cannot use AppState when `isAvailable` is false.');
}
switch (type) {
case 'change': {
case 'change':
// $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type
const changeHandler: AppStateValues => void = handler;
this._eventHandlers[type].set(
handler,
this.addListener('appStateDidChange', appStateData => {
changeHandler(appStateData.app_state);
}),
);
break;
}
case 'memoryWarning': {
return emitter.addListener('appStateDidChange', appStateData => {
changeHandler(appStateData.app_state);
});
case 'memoryWarning':
// $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type
const memoryWarningHandler: () => void = handler;
this._eventHandlers[type].set(
handler,
this.addListener('memoryWarning', memoryWarningHandler),
);
break;
}

return emitter.addListener('memoryWarning', memoryWarningHandler);
case 'blur':
case 'focus': {
case 'focus':
// $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type
const focusOrBlurHandler: () => void = handler;
this._eventHandlers[type].set(
handler,
this.addListener('appStateFocusChange', hasFocus => {
if (type === 'blur' && !hasFocus) {
focusOrBlurHandler();
}
if (type === 'focus' && hasFocus) {
focusOrBlurHandler();
}
}),
);
}
}
}

/**
* Remove a handler by passing the `change` event type and the handler.
*
* See https://reactnative.dev/docs/appstate.html#removeeventlistener
*/
removeEventListener<K: $Keys<AppStateEventDefinitions>>(
type: K,
handler: (...$ElementType<AppStateEventDefinitions, K>) => void,
) {
invariant(
this._supportedEvents.indexOf(type) !== -1,
'Trying to remove listener for unknown event: "%s"',
type,
);
const subscription = this._eventHandlers[type].get(handler);
if (!subscription) {
return;
return emitter.addListener('appStateFocusChange', hasFocus => {
if (type === 'blur' && !hasFocus) {
focusOrBlurHandler();
}
if (type === 'focus' && hasFocus) {
focusOrBlurHandler();
}
});
}
subscription.remove();
this._eventHandlers[type].delete(handler);
}
}

class MissingNativeModuleError extends Error {
constructor() {
super(
'Cannot use AppState module when native RCTAppState is not included in the build.\n' +
'Either include it, or check AppState.isAvailable before calling any methods.',
);
}
}

class MissingNativeAppStateShim extends EventEmitter<NativeAppStateEventDefinitions> {
// AppState
isAvailable: boolean = false;
currentState: ?string = null;

addEventListener<K: $Keys<AppStateEventDefinitions>>(
type: K,
handler: (...$ElementType<AppStateEventDefinitions, K>) => mixed,
): void {
throw new MissingNativeModuleError();
}

removeEventListener<K: $Keys<AppStateEventDefinitions>>(
type: K,
handler: (...$ElementType<AppStateEventDefinitions, K>) => mixed,
) {
throw new MissingNativeModuleError();
}

// $FlowIssue[invalid-tuple-arity]
addListener<K: $Keys<NativeAppStateEventDefinitions>>(
eventType: K,
// $FlowIssue[incompatible-extend]
listener: (...$ElementType<NativeAppStateEventDefinitions, K>) => mixed,
context: $FlowFixMe,
): EmitterSubscription<NativeAppStateEventDefinitions, K> {
throw new MissingNativeModuleError();
}

removeAllListeners<K: $Keys<NativeAppStateEventDefinitions>>(
eventType: ?K,
): void {
throw new MissingNativeModuleError();
}

removeSubscription<K: $Keys<NativeAppStateEventDefinitions>>(
subscription: EmitterSubscription<NativeAppStateEventDefinitions, K>,
): void {
throw new MissingNativeModuleError();
throw new Error('Trying to subscribe to unknown event: ' + type);
}
}

// This module depends on the native `RCTAppState` module. If you don't include it,
// `AppState.isAvailable` will return `false`, and any method calls will throw.
// We reassign the class variable to keep the autodoc generator happy.
const AppStateInstance: AppState | MissingNativeAppStateShim = NativeAppState
? new AppState()
: new MissingNativeAppStateShim();

module.exports = AppStateInstance;
module.exports = (new AppState(): AppState);
24 changes: 15 additions & 9 deletions packages/rn-tester/js/examples/AppState/AppStateExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

const React = require('react');

import {type EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter';
const {AppState, Text, View, Platform} = require('react-native');

class AppStateSubscription extends React.Component<
Expand All @@ -25,21 +26,26 @@ class AppStateSubscription extends React.Component<
eventsDetected: [],
};

_subscriptions: ?Array<EventSubscription>;

componentDidMount() {
AppState.addEventListener('change', this._handleAppStateChange);
AppState.addEventListener('memoryWarning', this._handleMemoryWarning);
this._subscriptions = [
AppState.addEventListener('change', this._handleAppStateChange),
AppState.addEventListener('memoryWarning', this._handleMemoryWarning),
];
if (Platform.OS === 'android') {
AppState.addEventListener('focus', this._handleFocus);
AppState.addEventListener('blur', this._handleBlur);
this._subscriptions.push(
AppState.addEventListener('focus', this._handleFocus),
AppState.addEventListener('blur', this._handleBlur),
);
}
}

componentWillUnmount() {
AppState.removeEventListener('change', this._handleAppStateChange);
AppState.removeEventListener('memoryWarning', this._handleMemoryWarning);
if (Platform.OS === 'android') {
AppState.removeEventListener('focus', this._handleFocus);
AppState.removeEventListener('blur', this._handleBlur);
if (this._subscriptions != null) {
for (const subscription of this._subscriptions) {
subscription.remove();
}
}
}

Expand Down

0 comments on commit 6f22989

Please sign in to comment.