Rishi
Rishi

Reputation: 2027

Platform specific import component in react native with typescript

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.

Upvotes: 32

Views: 11794

Answers (7)

Yogesh Mane
Yogesh Mane

Reputation: 601

Add the following line into tsconfig.json under compilerOptions:

"moduleSuffixes": [".ios", ".android", ".native", ""]

e.g.:

{
  ...
  "compilerOptions": {
    ...
    "moduleSuffixes": [".ios", ".android", ".native", ""]
  },
  ...
}

Upvotes: 24

user22445430
user22445430

Reputation: 21

Folder:

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

Test/index.android.tsx:

import React from 'react';
import { Text } from 'react-native';
import { TestProps } from './types';

function Test(props: TestProps) {
    return <Text>Test android</Text>;
}

export default Test;

Test/index.ios.tsx:

import React from 'react';
import { Text } from 'react-native';
import { TestProps } from './types';

function Test(props: TestProps) {
    return <Text>Test ios</Text>;
}

export default Test;

Test/index.d.ts:

import { TestProps } from './types';
export default function Test(props: TestProps): React.JSX.Element;

component/index.ts:

export { default as Test } from './components/Test';

use Test:

import React from 'react';
import { Test } from '@components';

function PageA() {
    return <Test />
}

export default PageA;

I've got this TypeScript code running without complaints and with the right types.

Upvotes: 2

Simon
Simon

Reputation: 1761

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

Upvotes: 0

peja
peja

Reputation: 875

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

Upvotes: 4

AnTSaSk
AnTSaSk

Reputation: 400

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>;
}

Upvotes: 8

Jacob Arvidsson
Jacob Arvidsson

Reputation: 638

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.

Upvotes: 38

Andreyco
Andreyco

Reputation: 22872

It all makes sense, since file at ../component/component.tsx does not exists.

Haven't tried, but this could learn TS to understand such imports

{
    "compilerOptions": {
        "paths": {
            "*": ["*", "*.ios", "*.android"]
        }
   }
}

Upvotes: 0

Related Questions