Creating cross-platform React projects by Will Acton / 10 November 2015

One of the touted perks of React/React Native is the ability to share most of your business logic across platforms. After being inspired by the Desktop, Mobile, and Web app in one project, I dove into how I could structure my project in a way to have a web, desktop (using Electron) and mobile app using the same codebase, with the idea that I could easily expand later into other platforms.

What's in a (file)name?

The first step is to recognize where the major differences between the mobile and web/desktop code take place: in the render() function inside our components. So, we need to abstract the rendering in a way that will allow our different platform-specific projects to find the correct code.

The easiest way to do this is to use an under-documented "feature" of React Native: when we import/require a file, e.g.:

import File from './File';  

React Native's packager will first look for File.<platform>.js, before it will look for File.js. This allows us to put our Android-specific code inside of File.android.js, iOS into File.ios.js, and our web code into just-plain File.js, without changing the files importing ./File.

Separate rendering from the component

NOTE: This strategy is most useful when we have components with complex state and logic that can be shared across platforms. In this example it is overkill, but I think the usefulness in an actual project is self-evident.

Lets look at a basic component:

class Hello extends Component {  
    constructor(props) {
       super(props);
    }
    render() {
        // Code to render a container with 
        // "Hello, {props.name}" inside of it
    }
}

In our web/electron app, our render function might look like this:

render() {  
    return (<div>Hello, {this.props.name}</div>);
}

And in our mobile app:

render() {  
    return (<View>
        <Text>Hello, {this.props.name}</text>
    </View>);
}

Let's see if we can separate them out without too much trouble. Let's structure our project like so (I'm only going to show this using Android for brevity and because I have tested it there, but the same should work for iOS):

- index.js
- index.android.js
- src/
-- Hello.js
-- HelloRender.js
-- HelloRender.android.js

And now we put our separate render functions, with dependencies in each (and some minor modifications):

// HelloRender.js
import React from 'react';

export default function (props) {  
    // we would like a pure function, for easy testing
    return (<div>Hello, {props.name}</div>);
}
// HelloRender.android.js
import React, {View, Text} from 'react-native';

export default function (props) {  
    return (<View>
        <Text>Hello, {props.name}</text>
    </View>);
}

And now finally, in our Hello.js:

// This will import our platform-specific render function
import Render from './HelloRender';

class Hello extends Component {  
    constructor(props) {
        super(props);
    }

    render() {
        // Now we call our render function,
        // bound to the context of our component
        // In this case we're only using component props,
        // but later we may desire to access the state or
        // other methods on the component.
        return Render.call(this, this.props, this.state);
    }
}

Component, and module clobbering

One thing that you may have noticed I left out above, is the line where we import Component into Hello.js. This is not an error; it deserves to be talked about on it's own.

On first glance, with the recent release of React v0.14 and the separation of react and react-dom, one would think that we could simply build our app using the react module until we needed something specifically from react-native. I have not found this to be so; I spent many hours trying to navigate and route around the errors that the React Native packager threw at me when I tried to use it with components defined with react dependencies. So my solution was to use a similar strategy as we used with abstracting away the render() function to also abstract away the platform-specific Component class.

Let us add two new files to our project:

- index.js
- index.android.js
- src/
-- Component.js
-- Component.android.js
-- Hello.js
-- HelloRender.js
-- HelloRender.android.js

We need a way to import the correct Component class for our platform. Here's what goes in each new file:

// Component.js
export {Component} from 'react';  
// Component.android.js
export {Component} from 'react-native';  

And now, in our Hello.js:

// import platform-specific component class
import {Component} from './Component';

class Hello extends Component ...  

VoilĂ ! The project should build without errors, and we are free to import our Hello component into our web, electron and mobile apps without any fuss, and continue to build the rest of our app using the same strategy.