Skip to content

Commit 53b1f69

Browse files
authored
Implement unstable_getBoundingClientRect in RN Fabric refs (#26137)
We're fixing the timing of layout and passive effects in React Native, and adding support for some Web APIs so common use cases for those effects can be implemented with the same code on React and React Native. Let's take this example: ```javascript function MyComponent(props) { const viewRef = useRef(); useLayoutEffect(() => { const rect = viewRef.current?.getBoundingClientRect(); console.log('My view is located at', rect?.toJSON()); }, []); return <View ref={viewRef}>{props.children}</View>; } ``` This could would work as expected on Web (ignoring the use of `View` and assuming something like `div`) but not on React Native because: 1. Layout is done asynchronously in a background thread in parallel with the execution of layout and passive effects. This is incorrect and it's being fixed in React Native (see facebook/react-native@afec07a). 2. We don't have an API to access layout information synchronously. The existing `ref.current.measureInWindow` uses callbacks to pass the result. That is asynchronous at the moment in Paper (the legacy renderer in React Native), but it's actually synchronous in Fabric (the new React Native renderer). This fixes point 2) by adding a Web-compatible method to access layout information (on Fabric only). This has 2 dependencies in React Native: 1. Access to `getBoundingClientRect` in Fabric, which was added in https://github.com/facebook/react-native/blob/main/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp#L644- L676 2. Access to `DOMRect`, which was added in facebook/react-native@673c761 . As next step, I'll modify the implementation of this and other methods in Fabric to warn when they're accessed during render. We can't do this on Web because we can't (shouldn't) modify built-in DOM APIs, but we can do it in React Native because the refs objects are built by the framework.
1 parent c0b0b3a commit 53b1f69

File tree

4 files changed

+79
-0
lines changed

4 files changed

+79
-0
lines changed

packages/react-native-renderer/src/ReactFabricHostConfig.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const {
5656
unstable_DiscreteEventPriority: FabricDiscretePriority,
5757
unstable_getCurrentEventPriority: fabricGetCurrentEventPriority,
5858
setNativeProps,
59+
getBoundingClientRect: fabricGetBoundingClientRect,
5960
} = nativeFabricUIManager;
6061

6162
const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
@@ -210,6 +211,20 @@ class ReactFabricHostComponent {
210211
}
211212
}
212213

214+
unstable_getBoundingClientRect(): DOMRect {
215+
const {stateNode} = this._internalInstanceHandle;
216+
if (stateNode != null) {
217+
const rect = fabricGetBoundingClientRect(stateNode.node);
218+
219+
if (rect) {
220+
return new DOMRect(rect[0], rect[1], rect[2], rect[3]);
221+
}
222+
}
223+
224+
// Empty rect if any of the above failed
225+
return new DOMRect(0, 0, 0, 0);
226+
}
227+
213228
setNativeProps(nativeProps: Object) {
214229
if (__DEV__) {
215230
warnForStyleProps(nativeProps, this.viewConfig.validAttributes);

packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,19 @@ const RCTFabricUIManager = {
149149

150150
callback(10, 10, 100, 100);
151151
}),
152+
getBoundingClientRect: jest.fn(function getBoundingClientRect(node) {
153+
if (typeof node !== 'object') {
154+
throw new Error(
155+
`Expected node to be an object, was passed "${typeof node}"`,
156+
);
157+
}
158+
159+
if (typeof node.viewName !== 'string') {
160+
throw new Error('Expected node to be a host node.');
161+
}
162+
163+
return [10, 10, 100, 100];
164+
}),
152165
measureLayout: jest.fn(function measureLayout(
153166
node,
154167
relativeNode,
@@ -181,3 +194,19 @@ const RCTFabricUIManager = {
181194
};
182195

183196
global.nativeFabricUIManager = RCTFabricUIManager;
197+
198+
// DOMRect isn't provided by jsdom, but it's used by `ReactFabricHostComponent`.
199+
// This is a basic implementation for testing.
200+
global.DOMRect = class DOMRect {
201+
constructor(x, y, width, height) {
202+
this.x = x;
203+
this.y = y;
204+
this.width = width;
205+
this.height = height;
206+
}
207+
208+
toJSON() {
209+
const {x, y, width, height} = this;
210+
return {x, y, width, height};
211+
}
212+
};

packages/react-native-renderer/src/__tests__/ReactFabricHostComponent-test.internal.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,33 @@ describe('measureLayout', () => {
200200
});
201201
});
202202

203+
describe('unstable_getBoundingClientRect', () => {
204+
test('component.unstable_getBoundingClientRect() returns DOMRect', () => {
205+
const [[fooRef]] = mockRenderKeys([['foo']]);
206+
207+
const rect = fooRef.unstable_getBoundingClientRect();
208+
209+
expect(nativeFabricUIManager.getBoundingClientRect).toHaveBeenCalledTimes(
210+
1,
211+
);
212+
expect(rect.toJSON()).toMatchObject({
213+
x: 10,
214+
y: 10,
215+
width: 100,
216+
height: 100,
217+
});
218+
});
219+
220+
test('unmounted.unstable_getBoundingClientRect() returns empty DOMRect', () => {
221+
const [[fooRef]] = mockRenderKeys([['foo'], null]);
222+
223+
const rect = fooRef.unstable_getBoundingClientRect();
224+
225+
expect(nativeFabricUIManager.getBoundingClientRect).not.toHaveBeenCalled();
226+
expect(rect.toJSON()).toMatchObject({x: 0, y: 0, width: 0, height: 0});
227+
});
228+
});
229+
203230
describe('setNativeProps', () => {
204231
test('setNativeProps(...) invokes setNativeProps on Fabric UIManager', () => {
205232
const {

scripts/flow/react-native-host-hooks.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ declare var nativeFabricUIManager: {
201201
onFail: () => void,
202202
onSuccess: __MeasureLayoutOnSuccessCallback,
203203
) => void,
204+
getBoundingClientRect: (
205+
node: Node,
206+
) => [
207+
/* x:*/ number,
208+
/* y:*/ number,
209+
/* width:*/ number,
210+
/* height:*/ number,
211+
],
204212
findNodeAtPoint: (
205213
node: Node,
206214
locationX: number,

0 commit comments

Comments
 (0)