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
15/**
16 * A map that caches registered native components.
17 */
18const nativeComponentsCache = new Map<string, HostComponent<any>>();
19
20/**
21 * Requires a React Native component from cache if possible. This prevents
22 * "Tried to register two views with the same name" errors on fast refresh, but
23 * also when there are multiple versions of the same package with native component.
24 */
25function requireCachedNativeComponent<Props>(viewName: string): HostComponent<Props> {
26  const cachedNativeComponent = nativeComponentsCache.get(viewName);
27
28  if (!cachedNativeComponent) {
29    const nativeComponent = requireNativeComponent<Props>(viewName);
30    nativeComponentsCache.set(viewName, nativeComponent);
31    return nativeComponent;
32  }
33  return cachedNativeComponent;
34}
35
36/**
37 * A drop-in replacement for `requireNativeComponent`.
38 */
39export function requireNativeViewManager<P>(viewName: string): React.ComponentType<P> {
40  const { viewManagersMetadata } = NativeModules.NativeUnimoduleProxy;
41  const viewManagerConfig = viewManagersMetadata?.[viewName];
42
43  if (__DEV__ && !viewManagerConfig) {
44    const exportedViewManagerNames = Object.keys(viewManagersMetadata).join(', ');
45    console.warn(
46      `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}].`
47    );
48  }
49
50  // Set up the React Native native component, which is an adapter to the universal module's view
51  // manager
52  const reactNativeViewName = `ViewManagerAdapter_${viewName}`;
53  const ReactNativeComponent = requireCachedNativeComponent(reactNativeViewName);
54
55  class NativeComponent extends React.PureComponent<P> {
56    static displayName = viewName;
57
58    // This will be accessed from native when the prototype functions are called,
59    // in order to find the associated native view.
60    nativeTag: number | null = null;
61
62    componentDidMount(): void {
63      this.nativeTag = findNodeHandle(this);
64    }
65
66    render(): React.ReactNode {
67      return <ReactNativeComponent {...this.props} />;
68    }
69  }
70
71  try {
72    const nativeModule = requireNativeModule(viewName);
73    const nativeViewPrototype = nativeModule.ViewPrototype;
74
75    if (nativeViewPrototype) {
76      // Assign native view functions to the component prototype so they can be accessed from the ref.
77      Object.assign(NativeComponent.prototype, nativeViewPrototype);
78    }
79  } catch {
80    // `requireNativeModule` may throw an error when the native module cannot be found.
81    // In some tests we don't mock the entire modules, but we do want to mock native views. For now,
82    // until we still have to support the legacy modules proxy and don't have better ways to mock,
83    // let's just gracefully skip assigning the prototype functions.
84    // See: https://github.com/expo/expo/blob/main/packages/expo-modules-core/src/__tests__/NativeViewManagerAdapter-test.native.tsx
85  }
86
87  return NativeComponent;
88}
89