Skip to content

Commit 63fccb8

Browse files
author
Ahmed Awaad
committed
Add useScreenDimensions hook
1 parent bf62cce commit 63fccb8

7 files changed

Lines changed: 172 additions & 1 deletion

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 {ReactTestRenderer} from 'react-test-renderer';
12+
13+
import Dimensions from '../Dimensions';
14+
import useScreenDimensions from '../useScreenDimensions';
15+
import {create, unmount} from '@react-native/jest-preset/jest/renderer';
16+
import * as React from 'react';
17+
import {act} from 'react-test-renderer';
18+
19+
describe('useScreenDimensions', () => {
20+
const window = {width: 320, height: 480, scale: 2, fontScale: 1};
21+
const screen = {width: 400, height: 800, scale: 2, fontScale: 1};
22+
let renderer: ?ReactTestRenderer;
23+
let observed;
24+
let renderCount;
25+
let originalDimensions;
26+
27+
function TestComponent() {
28+
observed = useScreenDimensions();
29+
renderCount++;
30+
return null;
31+
}
32+
33+
beforeEach(() => {
34+
originalDimensions = {
35+
window: Dimensions.get('window'),
36+
screen: Dimensions.get('screen'),
37+
};
38+
Dimensions.set({window, screen});
39+
renderCount = 0;
40+
});
41+
42+
afterEach(async () => {
43+
if (renderer != null) {
44+
await unmount(renderer);
45+
renderer = null;
46+
}
47+
jest.restoreAllMocks();
48+
Dimensions.set(originalDimensions);
49+
});
50+
51+
it('returns screen metrics rather than window metrics', async () => {
52+
renderer = await create(<TestComponent />);
53+
expect(observed).toEqual(screen);
54+
expect(observed).not.toEqual(window);
55+
});
56+
57+
it.each([
58+
['width', 800],
59+
['height', 400],
60+
['scale', 3],
61+
['fontScale', 1.5],
62+
])('updates when %s changes', async (property, value) => {
63+
renderer = await create(<TestComponent />);
64+
const nextScreen = {...screen, [property]: value};
65+
await act(() => {
66+
Dimensions.set({window, screen: nextScreen});
67+
});
68+
expect(observed).toEqual(nextScreen);
69+
});
70+
71+
it('does not render again for window-only changes or identical screen metrics', async () => {
72+
renderer = await create(<TestComponent />);
73+
const previousRenderCount = renderCount;
74+
const previousDimensions = observed;
75+
await act(() => {
76+
Dimensions.set({window: {...window, width: 200}, screen: {...screen}});
77+
});
78+
expect(renderCount).toBe(previousRenderCount);
79+
expect(observed).toBe(previousDimensions);
80+
});
81+
82+
it('catches changes between render and subscription', async () => {
83+
const nextScreen = {...screen, width: 800, height: 400};
84+
function UpdateBeforeSubscription() {
85+
React.useLayoutEffect(() => {
86+
Dimensions.set({window, screen: nextScreen});
87+
}, []);
88+
return <TestComponent />;
89+
}
90+
renderer = await create(<UpdateBeforeSubscription />);
91+
expect(observed).toEqual(nextScreen);
92+
});
93+
94+
it('removes its subscription on unmount', async () => {
95+
const addEventListener = jest.spyOn(Dimensions, 'addEventListener');
96+
renderer = await create(<TestComponent />);
97+
const subscription = addEventListener.mock.results[0].value;
98+
const remove = jest.spyOn(subscription, 'remove');
99+
await unmount(renderer);
100+
renderer = null;
101+
expect(remove).toHaveBeenCalledTimes(1);
102+
});
103+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 Dimensions from './Dimensions';
12+
import {
13+
type DisplayMetrics,
14+
type DisplayMetricsAndroid,
15+
} from './NativeDeviceInfo';
16+
import {useEffect, useState} from 'react';
17+
18+
/**
19+
* React hook that provides the screen's width, height, scale, and
20+
* font scale. Automatically updates when screen size or font scale changes.
21+
*
22+
* Width and height are reported in logical pixels, as in `Dimensions.get('screen')`.
23+
*/
24+
export default function useScreenDimensions():
25+
DisplayMetrics | DisplayMetricsAndroid {
26+
const [dimensions, setDimensions] = useState(() => Dimensions.get('screen'));
27+
useEffect(() => {
28+
function handleChange({
29+
screen,
30+
}: Readonly<{
31+
screen: DisplayMetrics | DisplayMetricsAndroid,
32+
...
33+
}>) {
34+
if (
35+
dimensions.width !== screen.width ||
36+
dimensions.height !== screen.height ||
37+
dimensions.scale !== screen.scale ||
38+
dimensions.fontScale !== screen.fontScale
39+
) {
40+
setDimensions(screen);
41+
}
42+
}
43+
const subscription = Dimensions.addEventListener('change', handleChange);
44+
// We might have missed an update between calling `get` in render and
45+
// `addEventListener` in this handler, so we set it here. If there was
46+
// no change, React will filter out this update as a no-op.
47+
handleChange({screen: Dimensions.get('screen')});
48+
return () => {
49+
subscription.remove();
50+
};
51+
}, [dimensions]);
52+
return dimensions;
53+
}

‎packages/react-native/ReactNativeApi.d.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<3ce9599aceb36dd1f09ded3db3332daa>>
7+
* @generated SignedSource<<daf08769b6e4279ce098d8147e0dc5ef>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -5437,6 +5437,7 @@ declare function useColorScheme(): ColorSchemeName | null
54375437
declare function usePressability(
54385438
config: null | PressabilityConfig | undefined,
54395439
): null | PressabilityEventHandlers
5440+
declare function useScreenDimensions(): DisplayMetrics | DisplayMetricsAndroid
54405441
declare function useWindowDimensions(): DisplayMetrics | DisplayMetricsAndroid
54415442
declare type UTFSequence = typeof UTFSequence
54425443
declare type Value = null | {
@@ -6068,5 +6069,6 @@ export {
60686069
useAnimatedValueXY, // dc9057ea
60696070
useColorScheme, // d585efdb
60706071
usePressability, // af291ad3
6072+
useScreenDimensions, // a8bd8ad8
60716073
useWindowDimensions, // bb4b683f
60726074
}

‎packages/react-native/__typetests__/index.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ import {
121121
requireNativeComponent,
122122
useColorScheme,
123123
useWindowDimensions,
124+
useScreenDimensions,
124125
// @ts-ignore
125126
SectionListData,
126127
ToastAndroid,
@@ -165,6 +166,11 @@ function testDimensions() {
165166
subscription.remove();
166167
}
167168

169+
function TestUseScreenDimensions() {
170+
const {width, height, scale, fontScale} = useScreenDimensions();
171+
const metrics: number[] = [width, height, scale, fontScale];
172+
}
173+
168174
function TextUseWindowDimensions() {
169175
const {width, height, scale, fontScale} = useWindowDimensions();
170176
}

‎packages/react-native/index.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,9 @@ module.exports = {
386386
get usePressability() {
387387
return require('./Libraries/Pressability/usePressability').default;
388388
},
389+
get useScreenDimensions() {
390+
return require('./Libraries/Utilities/useScreenDimensions').default;
391+
},
389392
get useWindowDimensions() {
390393
return require('./Libraries/Utilities/useWindowDimensions').default;
391394
},

‎packages/react-native/index.js.flow‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ export type {
457457
} from './Libraries/Pressability/Pressability';
458458
export {default as usePressability} from './Libraries/Pressability/usePressability';
459459
export {default as useColorScheme} from './Libraries/Utilities/useColorScheme';
460+
export {default as useScreenDimensions} from './Libraries/Utilities/useScreenDimensions';
460461
export {default as useWindowDimensions} from './Libraries/Utilities/useWindowDimensions';
461462
export {default as UTFSequence} from './Libraries/UTFSequence';
462463
export {default as Vibration} from './Libraries/Vibration/Vibration';

‎packages/react-native/types_DEPRECATED/Libraries/Utilities/Dimensions.d.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,7 @@ export interface Dimensions {
7676

7777
export function useWindowDimensions(): ScaledSize;
7878

79+
/** Returns screen dimensions and updates when they change. */
80+
export function useScreenDimensions(): ScaledSize;
81+
7982
export const Dimensions: Dimensions;

0 commit comments

Comments
 (0)