1import React from 'react';
2import { findNodeHandle, NativeModules, requireNativeComponent, HostComponent } from 'react-native';
3
4import { requireNativeModule } from './requireNativeModule';
5
6// To make the transition from React Native's `requireNativeComponent` to Expo's
7// `requireNativeViewManager` as easy as possible, `requireNativeViewManager` is a drop-in
8// replacement for `requireNativeComponent`.
9//
10// For each view manager, we create a wrapper component that accepts all of the props available to
11// the author of the universal module. This wrapper component splits the props into two sets: props
12// passed to React Native's View (ex: style, testID) and custom view props, which are passed to the
13// adapter view component in a prop called `proxiedProperties`.
14
15type NativeExpoComponentProps = {
16  proxiedProperties: object;
17};
18
19/**
20 * A map that caches registered native components.
21 */
22const nativeComponentsCache = new Map<string, HostComponent<any>>();
23
24/**
25 * Requires a React Native component from cache if possible. This prevents
26 * "Tried to register two views with the same name" errors on fast refresh, but
27 * also when there are multiple versions of the same package with native component.
28 */
29function requireCachedNativeComponent<Props>(viewName: string): HostComponent<Props> {
30  const cachedNativeComponent = nativeComponentsCache.get(viewName);
31
32  if (!cachedNativeComponent) {
33    const nativeComponent = requireNativeComponent<Props>(viewName);
34    nativeComponentsCache.set(viewName, nativeComponent);
35    return nativeComponent;
36  }
37  return cachedNativeComponent;
38}
39
40/**
41 * A drop-in replacement for `requireNativeComponent`.
42 */
43export function requireNativeViewManager<P>(viewName: string): React.ComponentType<P> {
44  const { viewManagersMetadata } = NativeModules.NativeUnimoduleProxy;
45  const viewManagerConfig = viewManagersMetadata?.[viewName];
46
47  if (__DEV__ && !viewManagerConfig) {
48    const exportedViewManagerNames = Object.keys(viewManagersMetadata).join(', ');
49    console.warn(
50      `The native view manager required by name (${viewName}) from NativeViewManagerAdapter isn't exported by expo-modules-core. Views of this type may not render correctly. Exported view managers: [${exportedViewManagerNames}].`
51    );
52  }
53
54  // Set up the React Native native component, which is an adapter to the universal module's view
55  // manager
56  const reactNativeViewName = `ViewManagerAdapter_${viewName}`;
57  const ReactNativeComponent =
58    requireCachedNativeComponent<NativeExpoComponentProps>(reactNativeViewName);
59  const proxiedPropsNames = viewManagerConfig?.propsNames ?? [];
60
61  class NativeComponent extends React.PureComponent<P> {
62    static displayName = viewName;
63
64    // This will be accessed from native when the prototype functions are called,
65    // in order to find the associated native view.
66    nativeTag: number | null = null;
67
68    componentDidMount(): void {
69      this.nativeTag = findNodeHandle(this);
70    }
71
72    render(): React.ReactNode {
73      const nativeProps = omit(this.props, proxiedPropsNames);
74      const proxiedProps = pick(this.props, proxiedPropsNames);
75
76      return <ReactNativeComponent {...nativeProps} proxiedProperties={proxiedProps} />;
77    }
78  }
79
80  try {
81    const nativeModule = requireNativeModule(viewName);
82    const nativeViewPrototype = nativeModule.ViewPrototype;
83
84    if (nativeViewPrototype) {
85      // Temporarily each function is wrapped to pass the native tag as the argument.
86      // In the future, native will automatically get the tag from `this` and then we can simply use
87      // `Object.assign(NativeComponent.prototype, nativeViewPrototype)` instead.
88      for (const key of Object.getOwnPropertyNames(nativeViewPrototype)) {
89        NativeComponent.prototype[key] = function (...args) {
90          return nativeViewPrototype[key].call(this, this.nativeTag, ...args);
91        };
92      }
93    }
94  } catch {
95    // `requireNativeModule` may throw an error when the native module cannot be found.
96    // In some tests we don't mock the entire modules, but we do want to mock native views. For now,
97    // until we still have to support the legacy modules proxy and don't have better ways to mock,
98    // let's just gracefully skip assigning the prototype functions.
99    // See: https://github.com/expo/expo/blob/main/packages/expo-modules-core/src/__tests__/NativeViewManagerAdapter-test.native.tsx
100  }
101
102  return NativeComponent;
103}
104
105function omit(props: Record<string, any>, propNames: string[]) {
106  const copied = { ...props };
107  for (const propName of propNames) {
108    delete copied[propName];
109  }
110  return copied;
111}
112
113function pick(props: Record<string, any>, propNames: string[]) {
114  return propNames.reduce((prev, curr) => {
115    if (curr in props) {
116      prev[curr] = props[curr];
117    }
118    return prev;
119  }, {});
120}
121