Platform specific import component in react native with typescript

Viewed 5805

I am using react native with typescript. I have a component with the following structure

component
    - component.ios.tsx
    - component.android.tsx

Now I want to import the component. So, I am doing this:

import component from '../component/component';

But it says:

[ts] Cannot find module ../component/component

The location is correct. I need to add something to the tslint file to make it understand.

I also tried to do:

let component = require('../component/component');

This didn't give typescript error. But it gave a runtime error.

element type is invalid expected a string or a class/function but got undefined

Has anybody run into this issue? Thank you.

5 Answers

One way of doing it, which is a bit annoying, is creating a declaration file with the same name.

component
- component.d.ts   <---
- component.android.tsx
- component.ios.tsx

then

import { file } from "../group/file";

Update: Another way of doing it is just omit the extension for one of them, then typescript will pick up the default one.

  • component.tsx
  • component.android.tsx

Android will pick up the specific android file, and iOS will default to normal one.

With last version of React you can use Suspense and lazy to avoid over typings etc., for example if I want a component Touchable with specific code for iOS and Android, my structure will looks like that:

- Touchable
   - index.tsx
   - Touchable.android.tsx
   - Touchable.ios.tsx
   - types.d.ts
   

And on index.tsx the code will looks like that:

import React, { FunctionComponent, lazy, Suspense } from 'react';
import { Platform, View } from 'react-native';

import { TouchableProps } from './types.d';

const TouchableComponent = lazy(() =>
  Platform.OS === 'ios'
    ? import('./Touchable.ios')
    : import('./Touchable.android'),
);

const Touchable: FunctionComponent<TouchableProps> = (props) => {
  return (
    <Suspense fallback={<View />}>
      <TouchableComponent {...props} />
    </Suspense>
  );
};

export default Touchable;

So anywhere on my app I want to use this component, I just have to do that:

import Touchable from './path/to/Touchable';

[...]
<Touchable>
  <Text>Touchable text</Text>
</Touchable>
[...]

Types.d.ts :

export type TouchableSizeType = 'small' | 'regular' | 'big';
export type TouchableType = 'primary' | 'secondary' | 'success' | 'error' | 'warning';

export interface TouchableProps {
  disabled?: boolean;
  size?: TouchableSizeType;
  type?: TouchableType;
  onClick?: (event?: Event) => Promise<void>;
}

Lets say that your component name is ProgressBar, so create index.d.ts and adjust your code below in index.d.ts

import {Component} from 'react';
import {Constructor, NativeMethodsMixin} from 'react-native';

export interface ProgressBarProps {
  progress: number;
}

declare class ProgressBarComponent extends Component<ProgressBarProps> {}

declare const ProgressBarBase: Constructor<NativeMethodsMixin> &
  typeof ProgressBarComponent;

export default class ProgressBar extends ProgressBarBase {}

So your component folder should looks like

component/
 - index.android.tsx
 - index.ios.tsx
 - index.d.ts

It's not the best solution since consumers of the component can still import the wrong things, but it's a step in the right direction. We metro bundler can find the proper files and we get type safety

Folder

  • PlatformImage
    • index.tsx
    • PlatformImage.android.tsx
    • PlatformImage.d.ts
    • PlatformImage.ios.tsx

Testing.tsx

import React from 'react';
import { StyleSheet } from 'react-native';
import PlatformImage from '/ui/corex/PlatformImage';

const Testing = () => {
  return (
    <PlatformImage
      style={styles.prefixImage}
      borderRadius={styles.prefixImage.borderRadius}
      source={{
        uri: 'my uri',
        priority: 'high',
      }}
    />
  );
};

const styles = StyleSheet.create({
  prefixImage: {
    resizeMode: 'contain',
    width: '100%',
    height: '100%',
    borderRadius: 40,
  },
});

export default Testing;

PlatformImage.d.ts

import { ImageStyle, StyleProp } from 'react-native';
import { Priority } from 'react-native-fast-image';

/**
 * An restricted list of props as we don't currently want to overcomplicate the code by supporting more
 * props than we actually need. If we need specific props that are supported by react-native.Image or
 * react-native-fast-image, we can add them as we need them.
 */
export interface PlatformImageProps {
  testID?: string;
  style?: StyleProp<ImageStyle>;
  source: {
    uri: string;
    /**
     * Only used in ios
     */
    priority: Priority;
  };
  /**
   *
   * Only used on android for border radius (since styles.borderRadius doesn't work for android). For ios, specify in styles.
   * Ideally the api for this component should be the same for both android & ios,
   * but this would mean preventing a styles.borderRadius from being used in ios in favor of using the
   * borderRadius prop, but this could be messy to code & maintain.
   */
  borderRadius?: number;
  resizeMode?: 'contain' | 'cover' | 'stretch' | 'center';
  /**
   * Invoked when load either succeeds or fails
   */
  onLoadEnd?: () => void;
}

declare const PlatformImage: React.ComponentType<PlatformImageProps>;

export default PlatformImage;

index.tsx

import PlatformImage from '/ui/corex/PlatformImage/PlatformImage';

export default PlatformImage;

PlatformImage.android.tsx

import React from 'react';
import { Image } from 'react-native';
import { PlatformImageProps } from '/ui/corex/PlatformImage/PlatformImage';

/**
 * Components should never import this directly, but should rather import the index.tsx file
 */
const PlatformImage = ({
  testID,
  style,
  source,
  resizeMode = 'cover',
  borderRadius,
  onLoadEnd,
}: PlatformImageProps) => {
  console.log('running android');
  return (
    <Image
      testID={testID}
      // todo simon: fix this typescript error
      style={style}
      resizeMode={resizeMode}
      borderRadius={borderRadius}
      onLoadEnd={onLoadEnd}
      source={source}
    />
  );
};

export default PlatformImage;

PlatformImage.ios.tsx

import React from 'react';
import FastImage from 'react-native-fast-image';
import { PlatformImageProps } from '/ui/corex/PlatformImage/PlatformImage';

/**
 * Components should never import this directly, but should rather import the index.tsx file
 */
const PlatformImage = ({
  testID,
  style,
  source,
  resizeMode = 'cover',
  onLoadEnd,
}: PlatformImageProps) => {
  console.log('running ios');
  return (
    <FastImage
      testID={testID}
      // todo simon: fix this typescript error
      style={style}
      resizeMode={resizeMode}
      onLoadEnd={onLoadEnd}
      source={source}
    />
  );
};

export default PlatformImage;

The mistake-prone import. Notice how vscode suggests a bunch of imports but we should only ever do import PlatformImage from '/ui/corex/PlatformImage';

enter image description here

Related