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,103 @@
/**
* 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);
});
});
53 changes: 53 additions & 0 deletions packages/react-native/Libraries/Utilities/useScreenDimensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* 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 {useEffect, useState} from 'react';

/**
* 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 {
const [dimensions, setDimensions] = useState(() => Dimensions.get('screen'));
useEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick nit--can you switch to using useSyncExternalStore instead? That should handle the tearing that you had set up in the hook body

function handleChange({
screen,
}: Readonly<{
screen: DisplayMetrics | DisplayMetricsAndroid,
...
}>) {
if (
dimensions.width !== screen.width ||
dimensions.height !== screen.height ||
dimensions.scale !== screen.scale ||
dimensions.fontScale !== screen.fontScale
) {
setDimensions(screen);
}
}
const subscription = Dimensions.addEventListener('change', handleChange);
// We might have missed an update between calling `get` in render and
// `addEventListener` in this handler, so we set it here. If there was
// no change, React will filter out this update as a no-op.
handleChange({screen: Dimensions.get('screen')});
return () => {
subscription.remove();
};
}, [dimensions]);
return dimensions;
}
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