Skip to content

Commit

Permalink
[Web] Remove findNodeHandle function. (#3127)
Browse files Browse the repository at this point in the history
## Description

`findNodeHandle` is a utility function that allows us to get native view from React component. The problem is, that under the hood it uses `findDOMNode`, which is deprecated and [will be removed in React 19](https://react.dev/reference/react-dom/findDOMNode). To get rid of this function, I've written our implementation that meets our requirements. 

Also, for new API we had to introduce a little trick (thanks @j-piasecki) - we add wrapper `div` with `display: contents;` style and we pass a ref to this element. With that change, it is much easier to get required `HTMLElement` - we simply have to get `div` from `ref` and find first child (preferably the one without `display: contents;`)

>[!WARNING]
> You may find tat in `StrictMode` most of new API examples doesn't work. That's not really the case. If you check example from [this comment](#3127 (comment)) it works fine. What really don't work in these examples are the animations.

## Test plan

Tested that example app works and no `findNodeHandle` error is thrown in `StrictMode`
  • Loading branch information
m-bert authored Oct 8, 2024
1 parent 671b66f commit a6741a9
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 5 deletions.
3 changes: 3 additions & 0 deletions src/findNodeHandle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { findNodeHandle } from 'react-native';

export default findNodeHandle;
33 changes: 33 additions & 0 deletions src/findNodeHandle.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type GestureHandlerRef = {
viewTag: GestureHandlerRef;
current: HTMLElement;
};

export default function findNodeHandle(
viewRef: GestureHandlerRef | HTMLElement
): HTMLElement | number {
// Old API assumes that child handler is HTMLElement.
// However, if we nest handlers, we will get ref to another handler.
// In that case, we want to recursively call findNodeHandle with new handler viewTag (which can also be ref to another handler).
if ((viewRef as GestureHandlerRef)?.viewTag !== undefined) {
return findNodeHandle((viewRef as GestureHandlerRef).viewTag);
}

if (viewRef instanceof HTMLElement) {
if (viewRef.style.display === 'contents') {
return findNodeHandle(viewRef.firstChild as HTMLElement);
}

return viewRef;
}

// In new API, we receive ref object which `current` field points to wrapper `div` with `display: contents;`.
// We want to return the first descendant (in DFS order) that doesn't have this property.
let element = viewRef?.current;

while (element && element.style.display === 'contents') {
element = element.firstChild as HTMLElement;
}

return element;
}
3 changes: 2 additions & 1 deletion src/handlers/createHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
GestureEvent,
HandlerStateChangeEvent,
} from './gestureHandlerCommon';
import { filterConfig, findNodeHandle, scheduleFlushOperations } from './utils';
import { filterConfig, scheduleFlushOperations } from './utils';
import findNodeHandle from '../findNodeHandle';
import { ValueOf } from '../typeUtils';
import { deepEqual, isFabric, isJestEnv, tagMessage } from '../utils';
import { ActionType } from '../ActionType';
Expand Down
41 changes: 41 additions & 0 deletions src/handlers/gestures/GestureDetector/Wrap.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { forwardRef } from 'react';
import type { LegacyRef, PropsWithChildren } from 'react';
import { tagMessage } from '../../../utils';

export const Wrap = forwardRef<HTMLDivElement, PropsWithChildren<{}>>(
({ children }, ref) => {
try {
// I don't think that fighting with types over such a simple function is worth it
// The only thing it does is add 'collapsable: false' to the child component
// to make sure it is in the native view hierarchy so the detector can find
// correct viewTag to attach to.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const child: any = React.Children.only(children);

const clone = React.cloneElement(
child,
{ collapsable: false },
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
child.props.children
);

return (
<div
ref={ref as LegacyRef<HTMLDivElement>}
style={{ display: 'contents' }}>
{clone}
</div>
);
} catch (e) {
throw new Error(
tagMessage(
`GestureDetector got more than one view as a child. If you want the gesture to work on multiple views, wrap them with a common parent and attach the gesture to that view.`
)
);
}
}
);

// On web we never take a path with Reanimated,
// therefore we can simply export Wrap
export const AnimatedWrap = Wrap;
3 changes: 2 additions & 1 deletion src/handlers/gestures/GestureDetector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import React, {
useMemo,
useRef,
} from 'react';
import { Platform, findNodeHandle } from 'react-native';
import { Platform } from 'react-native';
import findNodeHandle from '../../../findNodeHandle';
import { GestureType } from '../gesture';
import { UserSelect, TouchAction } from '../../gestureHandlerCommon';
import { ComposedGesture } from '../gestureComposition';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useCallback } from 'react';
import { findNodeHandle } from 'react-native';
import { GestureType } from '../gesture';
import { ComposedGesture } from '../gestureComposition';

Expand All @@ -13,6 +12,7 @@ import { updateHandlers } from './updateHandlers';
import { needsToReattach } from './needsToReattach';
import { dropHandlers } from './dropHandlers';
import { useForceRender, validateDetectorChildren } from './utils';
import findNodeHandle from '../../../findNodeHandle';

// Returns a function that's responsible for updating the attached gestures
// If the view has changed, it will reattach the handlers to the new view
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/gestures/GestureDetector/useViewRefHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getShadowNodeFromRef } from '../../../getShadowNodeFromRef';

import { GestureDetectorState } from './types';
import React, { useCallback } from 'react';
import { findNodeHandle } from 'react-native';
import findNodeHandle from '../../../findNodeHandle';

declare const global: {
isFormsStackingContext: (node: unknown) => boolean | null; // JSI function
Expand Down
2 changes: 1 addition & 1 deletion src/web/tools/GestureHandlerWebDelegate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { findNodeHandle } from 'react-native';
import findNodeHandle from '../../findNodeHandle';
import type IGestureHandler from '../handlers/IGestureHandler';
import {
GestureHandlerDelegate,
Expand Down

0 comments on commit a6741a9

Please sign in to comment.