Skip to content

Commit

Permalink
Implement unstable_getBoundingClientRect in RN Fabric refs (#26137)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rubennorte authored Feb 9, 2023
1 parent c0b0b3a commit 53b1f69
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 0 deletions.
15 changes: 15 additions & 0 deletions packages/react-native-renderer/src/ReactFabricHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const {
unstable_DiscreteEventPriority: FabricDiscretePriority,
unstable_getCurrentEventPriority: fabricGetCurrentEventPriority,
setNativeProps,
getBoundingClientRect: fabricGetBoundingClientRect,
} = nativeFabricUIManager;

const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
Expand Down Expand Up @@ -210,6 +211,20 @@ class ReactFabricHostComponent {
}
}

unstable_getBoundingClientRect(): DOMRect {
const {stateNode} = this._internalInstanceHandle;
if (stateNode != null) {
const rect = fabricGetBoundingClientRect(stateNode.node);

if (rect) {
return new DOMRect(rect[0], rect[1], rect[2], rect[3]);
}
}

// Empty rect if any of the above failed
return new DOMRect(0, 0, 0, 0);
}

setNativeProps(nativeProps: Object) {
if (__DEV__) {
warnForStyleProps(nativeProps, this.viewConfig.validAttributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ const RCTFabricUIManager = {

callback(10, 10, 100, 100);
}),
getBoundingClientRect: jest.fn(function getBoundingClientRect(node) {
if (typeof node !== 'object') {
throw new Error(
`Expected node to be an object, was passed "${typeof node}"`,
);
}

if (typeof node.viewName !== 'string') {
throw new Error('Expected node to be a host node.');
}

return [10, 10, 100, 100];
}),
measureLayout: jest.fn(function measureLayout(
node,
relativeNode,
Expand Down Expand Up @@ -181,3 +194,19 @@ const RCTFabricUIManager = {
};

global.nativeFabricUIManager = RCTFabricUIManager;

// DOMRect isn't provided by jsdom, but it's used by `ReactFabricHostComponent`.
// This is a basic implementation for testing.
global.DOMRect = class DOMRect {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}

toJSON() {
const {x, y, width, height} = this;
return {x, y, width, height};
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,33 @@ describe('measureLayout', () => {
});
});

describe('unstable_getBoundingClientRect', () => {
test('component.unstable_getBoundingClientRect() returns DOMRect', () => {
const [[fooRef]] = mockRenderKeys([['foo']]);

const rect = fooRef.unstable_getBoundingClientRect();

expect(nativeFabricUIManager.getBoundingClientRect).toHaveBeenCalledTimes(
1,
);
expect(rect.toJSON()).toMatchObject({
x: 10,
y: 10,
width: 100,
height: 100,
});
});

test('unmounted.unstable_getBoundingClientRect() returns empty DOMRect', () => {
const [[fooRef]] = mockRenderKeys([['foo'], null]);

const rect = fooRef.unstable_getBoundingClientRect();

expect(nativeFabricUIManager.getBoundingClientRect).not.toHaveBeenCalled();
expect(rect.toJSON()).toMatchObject({x: 0, y: 0, width: 0, height: 0});
});
});

describe('setNativeProps', () => {
test('setNativeProps(...) invokes setNativeProps on Fabric UIManager', () => {
const {
Expand Down
8 changes: 8 additions & 0 deletions scripts/flow/react-native-host-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ declare var nativeFabricUIManager: {
onFail: () => void,
onSuccess: __MeasureLayoutOnSuccessCallback,
) => void,
getBoundingClientRect: (
node: Node,
) => [
/* x:*/ number,
/* y:*/ number,
/* width:*/ number,
/* height:*/ number,
],
findNodeAtPoint: (
node: Node,
locationX: number,
Expand Down

0 comments on commit 53b1f69

Please sign in to comment.