Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/jest-preset/jest/mocks/useAppState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* 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
*/

import type {AppStateStatus} from 'react-native/Libraries/AppState/AppState';

const useAppState = jest.fn(() => 'active') as JestMockFn<
[],
AppStateStatus,
>;

export default useAppState;
5 changes: 5 additions & 0 deletions packages/jest-preset/jest/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ mock(
// $FlowFixMe[incompatible-type] - `./mocks/AppState` is incomplete.
'm#./mocks/AppState',
);
mock(
'm#react-native/Libraries/AppState/useAppState',
// $FlowFixMe[react-rule-hook-incompatible]
'm#./mocks/useAppState',
);
mock(
'm#react-native/Libraries/BatchedBridge/NativeModules',
'm#./mocks/NativeModules',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* 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
*/

import useAppState from '../useAppState';

describe('useAppState', () => {
it('should return a mocked active state by default', () => {
expect(jest.isMockFunction(useAppState)).toBe(true);
// $FlowFixMe[react-rule-hook]
expect(useAppState()).toBe('active');
});

it('should have console.error when not using mock', () => {
const useAppStateActual = jest.requireActual<{
default: typeof useAppState,
}>('../useAppState').default;
const spy = jest.spyOn(console, 'error').mockImplementationOnce(() => {
throw new Error('console.error() was called');
});

expect(() => {
// $FlowFixMe[react-rule-hook]
useAppStateActual();
}).toThrow();

expect(spy).toHaveBeenCalledWith(
expect.stringMatching(
/Invalid hook call. Hooks can only be called inside of the body of a function component./,
),
);
});
});
20 changes: 20 additions & 0 deletions packages/react-native/Libraries/AppState/useAppState.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

import type {AppStateStatus} from './AppState';

/**
* `useAppState` is a React hook that returns the current app state.
* The component will re-render whenever the app state changes.
*
* Returns one of: 'active', 'background', 'inactive', 'unknown', or 'extension'.
*
* @see https://reactnative.dev/docs/appstate
*/
export default function useAppState(): AppStateStatus;
58 changes: 58 additions & 0 deletions packages/react-native/Libraries/AppState/useAppState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* 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
*/

'use strict';

import type {AppStateStatus} from './AppState';

import AppState from './AppState';
import {useSyncExternalStore} from 'react';

const subscribe = (onStoreChange: () => void) => {
const subscription = AppState.addEventListener('change', onStoreChange);
return () => subscription.remove();
};

const getSnapshot = (): AppStateStatus => {
return AppState.currentState ?? 'unknown';
};

/**
* `useAppState` is a React hook that returns the current app state.
*
* The value will be one of:
* - `active` - The app is running in the foreground
* - `background` - The app is running in the background
* - `inactive` - (iOS only) Transitioning between foreground and background
* - `unknown` - The initial state before the app state is determined
*
* The hook automatically subscribes to app state changes and re-renders the
* component when the state changes.
*
* Usage:
* ```
* function MyComponent() {
* const appState = useAppState();
*
* useEffect(() => {
* if (appState === 'active') {
* // App came to foreground - refresh data
* }
* }, [appState]);
*
* return <Text>App is {appState}</Text>;
* }
* ```
*
* See https://reactnative.dev/docs/appstate
*/
export default function useAppState(): AppStateStatus {
return useSyncExternalStore(subscribe, getSnapshot);
}
3 changes: 3 additions & 0 deletions packages/react-native/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ module.exports = {
get useAnimatedColor() {
return require('./Libraries/Animated/useAnimatedColor').default;
},
get useAppState() {
return require('./Libraries/AppState/useAppState').default;
},
get useColorScheme() {
return require('./Libraries/Utilities/useColorScheme').default;
},
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ export type {
EventHandlers as PressabilityEventHandlers,
} from './Libraries/Pressability/Pressability';
export {default as usePressability} from './Libraries/Pressability/usePressability';
export {default as useAppState} from './Libraries/AppState/useAppState';
export {default as useColorScheme} from './Libraries/Utilities/useColorScheme';
export {default as useWindowDimensions} from './Libraries/Utilities/useWindowDimensions';
export {default as UTFSequence} from './Libraries/UTFSequence';
Expand Down
Loading