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 // Assign native view functions to the component prototype so they can be accessed from the ref. 86 Object.assign(NativeComponent.prototype, nativeViewPrototype); 87 } 88 } catch { 89 // `requireNativeModule` may throw an error when the native module cannot be found. 90 // In some tests we don't mock the entire modules, but we do want to mock native views. For now, 91 // until we still have to support the legacy modules proxy and don't have better ways to mock, 92 // let's just gracefully skip assigning the prototype functions. 93 // See: https://github.com/expo/expo/blob/main/packages/expo-modules-core/src/__tests__/NativeViewManagerAdapter-test.native.tsx 94 } 95 96 return NativeComponent; 97} 98 99function omit(props: Record<string, any>, propNames: string[]) { 100 const copied = { ...props }; 101 for (const propName of propNames) { 102 delete copied[propName]; 103 } 104 return copied; 105} 106 107function pick(props: Record<string, any>, propNames: string[]) { 108 return propNames.reduce((prev, curr) => { 109 if (curr in props) { 110 prev[curr] = props[curr]; 111 } 112 return prev; 113 }, {}); 114} 115