1import React from 'react';
2import { NativeModules, requireNativeComponent, HostComponent } from 'react-native';
3
4// To make the transition from React Native's `requireNativeComponent` to Expo's
5// `requireNativeViewManager` as easy as possible, `requireNativeViewManager` is a drop-in
6// replacement for `requireNativeComponent`.
7//
8// For each view manager, we create a wrapper component that accepts all of the props available to
9// the author of the universal module. This wrapper component splits the props into two sets: props
10// passed to React Native's View (ex: style, testID) and custom view props, which are passed to the
11// adapter view component in a prop called `proxiedProperties`.
12
13type NativeExpoComponentProps = {
14  proxiedProperties: object;
15};
16
17/**
18 * A map that caches registered native components.
19 */
20const nativeComponentsCache = new Map<string, HostComponent<any>>();
21
22/**
23 * Requires a React Native component from cache if possible. This prevents
24 * "Tried to register two views with the same name" errors on fast refresh, but
25 * also when there are multiple versions of the same package with native component.
26 */
27function requireCachedNativeComponent<Props>(viewName: string): HostComponent<Props> {
28  const cachedNativeComponent = nativeComponentsCache.get(viewName);
29
30  if (!cachedNativeComponent) {
31    const nativeComponent = requireNativeComponent<Props>(viewName);
32    nativeComponentsCache.set(viewName, nativeComponent);
33    return nativeComponent;
34  }
35  return cachedNativeComponent;
36}
37
38/**
39 * A drop-in replacement for `requireNativeComponent`.
40 */
41export function requireNativeViewManager<P>(viewName: string): React.ComponentType<P> {
42  const { viewManagersMetadata } = NativeModules.NativeUnimoduleProxy;
43  const viewManagerConfig = viewManagersMetadata?.[viewName];
44
45  if (__DEV__ && !viewManagerConfig) {
46    const exportedViewManagerNames = Object.keys(viewManagersMetadata).join(', ');
47    console.warn(
48      `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}].`
49    );
50  }
51
52  // Set up the React Native native component, which is an adapter to the universal module's view
53  // manager
54  const reactNativeViewName = `ViewManagerAdapter_${viewName}`;
55  const ReactNativeComponent =
56    requireCachedNativeComponent<NativeExpoComponentProps>(reactNativeViewName);
57  const proxiedPropsNames = viewManagerConfig?.propsNames ?? [];
58
59  // Define a component for universal-module authors to access their native view manager
60  const NativeComponentAdapter = React.forwardRef<any>((props, ref) => {
61    const nativeProps = omit(props, proxiedPropsNames);
62    const proxiedProps = pick(props, proxiedPropsNames);
63    return <ReactNativeComponent {...nativeProps} proxiedProperties={proxiedProps} ref={ref} />;
64  }) as React.ComponentType<P>;
65  NativeComponentAdapter.displayName = `Adapter<${viewName}>`;
66  return NativeComponentAdapter;
67}
68
69function omit(props: Record<string, any>, propNames: string[]) {
70  const copied = { ...props };
71  for (const propName of propNames) {
72    delete copied[propName];
73  }
74  return copied;
75}
76
77function pick(props: Record<string, any>, propNames: string[]) {
78  return propNames.reduce((prev, curr) => {
79    if (curr in props) {
80      prev[curr] = props[curr];
81    }
82    return prev;
83  }, {});
84}
85