Skip to content

Commit fdd4fae

Browse files
author
Ahmed Awaad
committed
Refactor useScreenDimensions hook to utilize useSyncExternalStore for improved performance and subscription management. Update tests to verify subscription behavior across screen updates and ensure correct metric reading when unmounted.
1 parent 63fccb8 commit fdd4fae

2 files changed

Lines changed: 53 additions & 28 deletions

File tree

‎packages/react-native/Libraries/Utilities/__tests__/useScreenDimensions-test.js‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,32 @@ describe('useScreenDimensions', () => {
100100
renderer = null;
101101
expect(remove).toHaveBeenCalledTimes(1);
102102
});
103+
104+
it('keeps its subscription across screen updates', async () => {
105+
const addEventListener = jest.spyOn(Dimensions, 'addEventListener');
106+
renderer = await create(<TestComponent />);
107+
const subscription = addEventListener.mock.results[0].value;
108+
const remove = jest.spyOn(subscription, 'remove');
109+
for (const width of [600, 800]) {
110+
await act(() => {
111+
Dimensions.set({window, screen: {...screen, width}});
112+
});
113+
expect(observed).toEqual({...screen, width});
114+
}
115+
expect(addEventListener).toHaveBeenCalledTimes(1);
116+
expect(remove).not.toHaveBeenCalled();
117+
await unmount(renderer);
118+
renderer = null;
119+
expect(remove).toHaveBeenCalledTimes(1);
120+
});
121+
122+
it('reads current metrics after changes while unmounted', async () => {
123+
renderer = await create(<TestComponent />);
124+
await unmount(renderer);
125+
renderer = null;
126+
const nextScreen = {...screen, width: 800};
127+
Dimensions.set({window, screen: nextScreen});
128+
renderer = await create(<TestComponent />);
129+
expect(observed).toEqual(nextScreen);
130+
});
103131
});

‎packages/react-native/Libraries/Utilities/useScreenDimensions.js‎

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,30 @@ import {
1313
type DisplayMetrics,
1414
type DisplayMetricsAndroid,
1515
} from './NativeDeviceInfo';
16-
import {useEffect, useState} from 'react';
16+
import {useSyncExternalStore} from 'react';
17+
18+
const subscribe = (onStoreChange: () => void) => {
19+
const subscription = Dimensions.addEventListener('change', onStoreChange);
20+
return () => subscription.remove();
21+
};
22+
23+
let cachedScreen: DisplayMetrics | DisplayMetricsAndroid | void;
24+
25+
function getSnapshot(): DisplayMetrics | DisplayMetricsAndroid {
26+
const screen = Dimensions.get('screen');
27+
// Dimensions can emit a new screen object without changing its metrics.
28+
// Preserve snapshot identity so those events do not trigger a render.
29+
if (
30+
cachedScreen == null ||
31+
cachedScreen.width !== screen.width ||
32+
cachedScreen.height !== screen.height ||
33+
cachedScreen.scale !== screen.scale ||
34+
cachedScreen.fontScale !== screen.fontScale
35+
) {
36+
cachedScreen = screen;
37+
}
38+
return cachedScreen;
39+
}
1740

1841
/**
1942
* React hook that provides the screen's width, height, scale, and
@@ -23,31 +46,5 @@ import {useEffect, useState} from 'react';
2346
*/
2447
export default function useScreenDimensions():
2548
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;
49+
return useSyncExternalStore(subscribe, getSnapshot);
5350
}

0 commit comments

Comments
 (0)