Skip to content

Commit 27c4fdf

Browse files
Add useAppState() hook for tracking app foreground/background state
Add a new React hook that lets components subscribe to app state changes using the modern useSyncExternalStore pattern, matching the existing useColorScheme and useWindowDimensions hooks. This solves a common pain point where developers must manually manage AppState event listeners and local state to know if the app is in foreground or background. Common use cases include: - Refreshing data when the app returns to foreground - Pausing media playback when backgrounded - Saving drafts when the user leaves - Disabling timers in background Usage: import { useAppState } from 'react-native'; function MyComponent() { const appState = useAppState(); // Returns: 'active' | 'background' | 'inactive' | 'unknown' }
1 parent 8bac1df commit 27c4fdf

File tree

7 files changed

+144
-0
lines changed

7 files changed

+144
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {AppStateStatus} from 'react-native/Libraries/AppState/AppState';
12+
13+
const useAppState = jest.fn(() => 'active') as JestMockFn<
14+
[],
15+
AppStateStatus,
16+
>;
17+
18+
export default useAppState;

packages/jest-preset/jest/setup.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ mock(
9191
// $FlowFixMe[incompatible-type] - `./mocks/AppState` is incomplete.
9292
'm#./mocks/AppState',
9393
);
94+
mock(
95+
'm#react-native/Libraries/AppState/useAppState',
96+
// $FlowFixMe[react-rule-hook-incompatible]
97+
'm#./mocks/useAppState',
98+
);
9499
mock(
95100
'm#react-native/Libraries/BatchedBridge/NativeModules',
96101
'm#./mocks/NativeModules',
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import useAppState from '../useAppState';
12+
13+
describe('useAppState', () => {
14+
it('should return a mocked active state by default', () => {
15+
expect(jest.isMockFunction(useAppState)).toBe(true);
16+
// $FlowFixMe[react-rule-hook]
17+
expect(useAppState()).toBe('active');
18+
});
19+
20+
it('should have console.error when not using mock', () => {
21+
const useAppStateActual = jest.requireActual<{
22+
default: typeof useAppState,
23+
}>('../useAppState').default;
24+
const spy = jest.spyOn(console, 'error').mockImplementationOnce(() => {
25+
throw new Error('console.error() was called');
26+
});
27+
28+
expect(() => {
29+
// $FlowFixMe[react-rule-hook]
30+
useAppStateActual();
31+
}).toThrow();
32+
33+
expect(spy).toHaveBeenCalledWith(
34+
expect.stringMatching(
35+
/Invalid hook call. Hooks can only be called inside of the body of a function component./,
36+
),
37+
);
38+
});
39+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
import type {AppStateStatus} from './AppState';
11+
12+
/**
13+
* `useAppState` is a React hook that returns the current app state.
14+
* The component will re-render whenever the app state changes.
15+
*
16+
* Returns one of: 'active', 'background', 'inactive', 'unknown', or 'extension'.
17+
*
18+
* @see https://reactnative.dev/docs/appstate
19+
*/
20+
export default function useAppState(): AppStateStatus;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
import type {AppStateStatus} from './AppState';
14+
15+
import AppState from './AppState';
16+
import {useSyncExternalStore} from 'react';
17+
18+
const subscribe = (onStoreChange: () => void) => {
19+
const subscription = AppState.addEventListener('change', onStoreChange);
20+
return () => subscription.remove();
21+
};
22+
23+
const getSnapshot = (): AppStateStatus => {
24+
return AppState.currentState ?? 'unknown';
25+
};
26+
27+
/**
28+
* `useAppState` is a React hook that returns the current app state.
29+
*
30+
* The value will be one of:
31+
* - `active` - The app is running in the foreground
32+
* - `background` - The app is running in the background
33+
* - `inactive` - (iOS only) Transitioning between foreground and background
34+
* - `unknown` - The initial state before the app state is determined
35+
*
36+
* The hook automatically subscribes to app state changes and re-renders the
37+
* component when the state changes.
38+
*
39+
* Usage:
40+
* ```
41+
* function MyComponent() {
42+
* const appState = useAppState();
43+
*
44+
* useEffect(() => {
45+
* if (appState === 'active') {
46+
* // App came to foreground - refresh data
47+
* }
48+
* }, [appState]);
49+
*
50+
* return <Text>App is {appState}</Text>;
51+
* }
52+
* ```
53+
*
54+
* See https://reactnative.dev/docs/appstate
55+
*/
56+
export default function useAppState(): AppStateStatus {
57+
return useSyncExternalStore(subscribe, getSnapshot);
58+
}

packages/react-native/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,9 @@ module.exports = {
375375
get useAnimatedColor() {
376376
return require('./Libraries/Animated/useAnimatedColor').default;
377377
},
378+
get useAppState() {
379+
return require('./Libraries/AppState/useAppState').default;
380+
},
378381
get useColorScheme() {
379382
return require('./Libraries/Utilities/useColorScheme').default;
380383
},

packages/react-native/index.js.flow

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ export type {
414414
EventHandlers as PressabilityEventHandlers,
415415
} from './Libraries/Pressability/Pressability';
416416
export {default as usePressability} from './Libraries/Pressability/usePressability';
417+
export {default as useAppState} from './Libraries/AppState/useAppState';
417418
export {default as useColorScheme} from './Libraries/Utilities/useColorScheme';
418419
export {default as useWindowDimensions} from './Libraries/Utilities/useWindowDimensions';
419420
export {default as UTFSequence} from './Libraries/UTFSequence';

0 commit comments

Comments
 (0)