Expo dynamic platform OS rendering with Typescript

12 jul 2022

Table of contents

Introduction

Expo is a great tool. Allowing anyone to create a react-native mobile and web app. But comes to a point where a lib you need is not supported by default on every OS you want to build on. Here is the list of OS where an expo app can be run:

/**
 * @see https://reactnative.dev/docs/platform-specific-code#content
 */
export type PlatformOSType =
  | "ios"
  | "android"
  | "macos"
  | "windows"
  | "web"
  | "native";

A solution would be to render specific react-native component using if or ternaries operator. It’s not recommended as it brings a double lecture to the code and is not idiomatic to expo. Expo provides a Platform module, which contains a select method that allow you to return anything depending on the platform OS.

Platform dynamic style declaration example:

import { Platform, StyleSheet } from "react-native";

const styles = StyleSheet.create({
  container: {
    flex: 1,
    ...Platform.select({
      android: {
        backgroundColor: "green",
      },
      ios: {
        backgroundColor: "red",
      },
      default: {
        // other platforms, web for example
        backgroundColor: "blue",
      },
    }),
  },
});

The map example

Here we want to display a map on both mobile and web, using expo MapView on mobile and react-native-maps on web. As we could see on the above example Platform.select have infinite use cases. For our map issue we wanna dynamically render react component, but also sharing the same typed props between each of them, let’s say the default map coords.

Dynamic component rendering example:

const Component = Platform.select({
  ios: () => require("ComponentIOS"),
  android: () => require("ComponentAndroid"),
})();

<Component />;

Platform OS specific component

Our component have to be sharing same typed props. Then we can establish the following types:

// contract.ts

export interface LatlngCoords {
  lat: number;
  lng: number;
}

export interface MapComponentProps {
  defaultMapCenterCoords: LatlngCoords;
}

export type MapFunctionComponent = React.FC<MapComponentProps>;

We want to display this component on web:

PS: note that div are then available in a web context.

// web.tsx

import GoogleMapReact from "google-map-react";
import React from "react";
import { MapFunctionComponent } from "./contract";

const WebMaps: MapFunctionComponent = ({ defaultMapCenterCoords }, _) => {
  return (
    <div style={{ height: "100vh", width: "100%" }}>
      <GoogleMapReact
        bootstrapURLKeys={{ key: "" }}
        defaultCenter={defaultMapCenterCoords}
        defaultZoom={9}
      ></GoogleMapReact>
    </div>
  );
};

export default WebMaps;

And we want to display this component on native:

// native.tsx

import React from "react";
import MapView from "react-native-maps";
import { MapFunctionComponent } from "./contract";

const NativeMaps: MapFunctionComponent = ({ defaultMapCenterCoords }) => {
  return (
    <MapView
      initialRegion={{
        latitude: defaultMapCenterCoords.lat,
        longitude: defaultMapCenterCoords.lng,
        latitudeDelta: 0.0922,
        longitudeDelta: 0.0421,
      }}
      style={{
        width: "100%",
        height: "100%",
      }}
      provider={"google"}
    />
  );
};

export default NativeMaps;

Now that we have both what we want to render on web and native, the last thing to do is to compute everything inside a Platform.select call and export the resulting component.

// index.tsx

import { Platform } from "react-native";
import { MapFunctionComponent } from "./contract";

const MapComponent: MapFunctionComponent = Platform.select({
  native: () => await import("./native").default,
  default: () => await import("./web").default,
})();

export default MapComponent;

export * from "./contract";

Finally, use the mapComponent wherever you want.

// App.tsx

import * as React from "react";
import { SafeAreaView } from "react-native";
import MapComponent from "./MapComponent";

export default function App() {
  const defaultMapCenterCoords = {
    lat: 48.866667,
    lng: 2.333333,
  };
  return (
    <SafeAreaView
      style={{
        flex: 1,
        backgroundColor: "#fff",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <MapComponent defaultMapCenterCoords={defaultMapCenterCoords} />
    </SafeAreaView>
  );
}

Ending

During discovering expo we could easily think that ejecting would be a required end at some point. But using expo is really smooth, it provides a lot of tools to answer specific needs idiomatically. Combining advanced react-native and platform.select usage, such as ExoticComponent to type safely forward refs can lead to powerful and readable architecture.

Working example

You can find a working example repository below expo-platform-select-example →

Thanks for reading. Any suggestions are welcomed !