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
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* 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 {ReactTestRenderer} from 'react-test-renderer';

import Dimensions from '../Dimensions';
import useScreenDimensions from '../useScreenDimensions';
import {create, unmount} from '@react-native/jest-preset/jest/renderer';
import * as React from 'react';
import {act} from 'react-test-renderer';

describe('useScreenDimensions', () => {
const window = {width: 320, height: 480, scale: 2, fontScale: 1};
const screen = {width: 400, height: 800, scale: 2, fontScale: 1};
let renderer: ?ReactTestRenderer;
let observed;
let renderCount;
let originalDimensions;

function TestComponent() {
observed = useScreenDimensions();
renderCount++;
return null;
}

beforeEach(() => {
originalDimensions = {
window: Dimensions.get('window'),
screen: Dimensions.get('screen'),
};
Dimensions.set({window, screen});
renderCount = 0;
});

afterEach(async () => {
if (renderer != null) {
await unmount(renderer);
renderer = null;
}
jest.restoreAllMocks();
Dimensions.set(originalDimensions);
});

it('returns screen metrics rather than window metrics', async () => {
renderer = await create(<TestComponent />);
expect(observed).toEqual(screen);
expect(observed).not.toEqual(window);
});

it.each([
['width', 800],
['height', 400],
['scale', 3],
['fontScale', 1.5],
])('updates when %s changes', async (property, value) => {
renderer = await create(<TestComponent />);
const nextScreen = {...screen, [property]: value};
await act(() => {
Dimensions.set({window, screen: nextScreen});
});
expect(observed).toEqual(nextScreen);
});

it('does not render again for window-only changes or identical screen metrics', async () => {
renderer = await create(<TestComponent />);
const previousRenderCount = renderCount;
const previousDimensions = observed;
await act(() => {
Dimensions.set({window: {...window, width: 200}, screen: {...screen}});
});
expect(renderCount).toBe(previousRenderCount);
expect(observed).toBe(previousDimensions);
});

it('catches changes between render and subscription', async () => {
const nextScreen = {...screen, width: 800, height: 400};
function UpdateBeforeSubscription() {
React.useLayoutEffect(() => {
Dimensions.set({window, screen: nextScreen});
}, []);
return <TestComponent />;
}
renderer = await create(<UpdateBeforeSubscription />);
expect(observed).toEqual(nextScreen);
});

it('removes its subscription on unmount', async () => {
const addEventListener = jest.spyOn(Dimensions, 'addEventListener');
renderer = await create(<TestComponent />);
const subscription = addEventListener.mock.results[0].value;
const remove = jest.spyOn(subscription, 'remove');
await unmount(renderer);
renderer = null;
expect(remove).toHaveBeenCalledTimes(1);
});

it('keeps its subscription across screen updates', async () => {
const addEventListener = jest.spyOn(Dimensions, 'addEventListener');
renderer = await create(<TestComponent />);
const subscription = addEventListener.mock.results[0].value;
const remove = jest.spyOn(subscription, 'remove');
for (const width of [600, 800]) {
await act(() => {
Dimensions.set({window, screen: {...screen, width}});
});
expect(observed).toEqual({...screen, width});
}
expect(addEventListener).toHaveBeenCalledTimes(1);
expect(remove).not.toHaveBeenCalled();
await unmount(renderer);
renderer = null;
expect(remove).toHaveBeenCalledTimes(1);
});

it('reads current metrics after changes while unmounted', async () => {
renderer = await create(<TestComponent />);
await unmount(renderer);
renderer = null;
const nextScreen = {...screen, width: 800};
Dimensions.set({window, screen: nextScreen});
renderer = await create(<TestComponent />);
expect(observed).toEqual(nextScreen);
});
});
50 changes: 50 additions & 0 deletions packages/react-native/Libraries/Utilities/useScreenDimensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* 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 Dimensions from './Dimensions';
import {
type DisplayMetrics,
type DisplayMetricsAndroid,
} from './NativeDeviceInfo';
import {useSyncExternalStore} from 'react';

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

let cachedScreen: DisplayMetrics | DisplayMetricsAndroid | void;

function getSnapshot(): DisplayMetrics | DisplayMetricsAndroid {
const screen = Dimensions.get('screen');
// Dimensions can emit a new screen object without changing its metrics.
// Preserve snapshot identity so those events do not trigger a render.
if (
cachedScreen == null ||
cachedScreen.width !== screen.width ||
cachedScreen.height !== screen.height ||
cachedScreen.scale !== screen.scale ||
cachedScreen.fontScale !== screen.fontScale
) {
cachedScreen = screen;
}
return cachedScreen;
}

/**
* React hook that provides the screen's width, height, scale, and
* font scale. Automatically updates when screen size or font scale changes.
*
* Width and height are reported in logical pixels, as in `Dimensions.get('screen')`.
*/
export default function useScreenDimensions():
DisplayMetrics | DisplayMetricsAndroid {
return useSyncExternalStore(subscribe, getSnapshot);
}
2 changes: 2 additions & 0 deletions packages/react-native/ReactNativeApi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5433,6 +5433,7 @@ declare function useColorScheme(): ColorSchemeName | null
declare function usePressability(
config: null | PressabilityConfig | undefined,
): null | PressabilityEventHandlers
declare function useScreenDimensions(): DisplayMetrics | DisplayMetricsAndroid
declare function useWindowDimensions(): DisplayMetrics | DisplayMetricsAndroid
declare type UTFSequence = typeof UTFSequence
declare type Value = null | {
Expand Down Expand Up @@ -6065,5 +6066,6 @@ export {
useAnimatedValueXY, // dc9057ea
useColorScheme, // d585efdb
usePressability, // af291ad3
useScreenDimensions, // a8bd8ad8
useWindowDimensions, // bb4b683f
}
6 changes: 6 additions & 0 deletions packages/react-native/__typetests__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import {
requireNativeComponent,
useColorScheme,
useWindowDimensions,
useScreenDimensions,
// @ts-ignore
SectionListData,
ToastAndroid,
Expand Down Expand Up @@ -165,6 +166,11 @@ function testDimensions() {
subscription.remove();
}

function TestUseScreenDimensions() {
const {width, height, scale, fontScale} = useScreenDimensions();
const metrics: number[] = [width, height, scale, fontScale];
}

function TextUseWindowDimensions() {
const {width, height, scale, fontScale} = useWindowDimensions();
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react-native/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,9 @@ module.exports = {
get usePressability() {
return require('./Libraries/Pressability/usePressability').default;
},
get useScreenDimensions() {
return require('./Libraries/Utilities/useScreenDimensions').default;
},
get useWindowDimensions() {
return require('./Libraries/Utilities/useWindowDimensions').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 @@ -460,6 +460,7 @@ export type {
} from './Libraries/Pressability/Pressability';
export {default as usePressability} from './Libraries/Pressability/usePressability';
export {default as useColorScheme} from './Libraries/Utilities/useColorScheme';
export {default as useScreenDimensions} from './Libraries/Utilities/useScreenDimensions';
export {default as useWindowDimensions} from './Libraries/Utilities/useWindowDimensions';
export {default as UTFSequence} from './Libraries/UTFSequence';
export {default as Vibration} from './Libraries/Vibration/Vibration';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,7 @@ export interface Dimensions {

export function useWindowDimensions(): ScaledSize;

/** Returns screen dimensions and updates when they change. */
export function useScreenDimensions(): ScaledSize;

export const Dimensions: Dimensions;
Loading