Write once, run anywhere with Create React (Native) App and react-native-web

October 25, 20175 min read

Since the first release of React Native, I’ve always tried to build a React codebase that could run on any platform.

Wow! Such engineering! Much platform! Amaze!

I loved building multi platform apps using Cordova but React Native has raised the bar so high now with its native performance.

So far, when you wanted to have a unified codebase of the Web and Native, you had to mess around with build systems (Webpack and React Native packager) in order to make everything work together.

Now with Create React Native App and Create React App, we do not have to worry about this layer of complexity.

Phase 1 : The dirty work

You’ll need :

  • the latest version of Node (when I wrote this post, it was 8.6.0)
  • create-react-app

First, create a React app using :

create-react-app my-hybrid-app && cd my-hybrid-app

We need to add a few dependencies to our project :

npm install --save-dev babel-plugin-transform-object-rest-spread babel-plugin-transform-react-jsx-source babel-preset-expo jest-expo flow-bin react-native-scripts [email protected]
npm install --save [email protected]^21.0.2 react [email protected]^0.48.4 react-native-web

Now let’s add a few files necessary to build a React Native app :

  • .babelrc
{
  "presets": ["babel-preset-expo"],
  "env": {
    "development": {
      "plugins": ["transform-object-rest-spread", "transform-react-jsx-source"]
    }
  }
}
  • .watchmanconfig
{}
  • .flowconfig
[ignore]
; We fork some components by platform
.*/*[.]android.js

; Ignore "BUCK" generated dirs
/\.buckd/

; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*

; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
.*/Libraries/react-native/React.js
.*/Libraries/react-native/ReactNative.js

; Additional create-react-native-app ignores

; Ignore duplicate module providers
.*/node_modules/fbemitter/lib/*

; Ignore misbehaving dev-dependencies
.*/node_modules/xdl/build/*
.*/node_modules/reqwest/tests/*

; Ignore missing expo-sdk dependencies (temporarily)
; https://github.com/expo/expo/issues/162
.*/node_modules/expo/src/*

; Ignore react-native-fbads dependency of the expo sdk
.*/node_modules/react-native-fbads/*

[include]

[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow
flow/

[options]
module.system=haste

emoji=true

experimental.strict_type_args=true

munge_underscores=true

module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'

suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FixMe

suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-9]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-9]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError

unsafe.enable_getters_and_setters=true

[version]
^0.49.1
  • app.json
{
  "expo": {
    "sdkVersion": "21.0.0"
  }
}
  • App.test.js : this is the entry point for testing the React Native app.
import React from 'react';
import App from './App';

import renderer from 'react-test-renderer';

it('renders without crashing', () => {
  const rendered = renderer.create(<App />).toJSON();
  expect(rendered).toBeTruthy();
});

And then add a App.js file, this is going to be our React Native app entry point.

import React from 'react'
import App from './src/App'

export default class NativeApp extends React.Component {
  render() {
    return <App />
  }
}

Then, let’s make a few changes to our package.json file and add some useful scripts :

// These scripts are merely copied from the create-react-native-app package.json file
...
"main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
"scripts": {
  "start-web": "react-scripts start",
  "build-web": "react-scripts build",
  "test": "react-scripts test --env=jsdom",
  "eject-web": "react-scripts eject",

  "start-native": "react-native-scripts start",
  "eject-native": "react-native-scripts eject",
  "android": "react-native-scripts android",
  "ios": "react-native-scripts ios",
  "test-native": "node node_modules/jest/bin/jest.js --watch",

  "test": "npm run test-web && npm run test-native",
},
"jest": {
  "preset": "jest-expo"
}
...

Phase 2 : Now let’s profit!

Everything is set up in our project, let’s start both packagers in two different terminal windows.

Terminal 1 :

npm run start-web

Terminal 2 :

npm run ios

We need to edit ./src/App.js and add some platform agnostic code in it.

You can now write all your code as if you were writing some React Native code.

The trick here is that the webpack config of Create React App aliases automatically react-native to react-native-web (see the config here).

Everything is handled for us!

import React, { Component } from 'react'
import { View, Text, StyleSheet } from 'react-native'

export default class App extends Component {
  render() {
    return (
      <View style={styles.app}>
        <View style={styles.appHeader}>
          <Text style={styles.appTitle}>Welcome to React ⚛️</Text>
        </View>
        <Text style={styles.appIntro}>
          To get started, edit src/App.js and save to reload.
        </Text>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  app: {
    flex: 1
  },
  appHeader: {
    flex: 1,
    backgroundColor: '#222',
    padding: 20,
    justifyContent: 'center',
    alignItems: 'center'
  },
  appTitle: {
    fontSize: 16,
    color: 'white'
  },
  appIntro: {
    flex: 2,
    fontSize: 30,
    textAlign: 'center'
  }
})

You should now see this :

Fig 1

The result on the web is not really the same as on the mobile. That’s because the #root DOM element styling needs to be tweaked to match its mobile counterpart :

To do so, open ./src/index.css and add

/* ... */

#root {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

And Tada!

Fig 2

You’ve built your first universal React components running on the Web, iOS and Android.

I did not invent anything here : I leveraged the amazing work done by the teams behind Create React App, Create React Native App, react-native-web and Expo.

You should check these projects out and support them! They help making React development a breeze.



author undefined

By Yannick Spark : a Front-end engineer who works remotely at Teacup Analytics.
He likes Functional programming, Nutrition & Fasting, and Remote work.
You should definitely on Twitter 👋