Your First Hybrid App in Minutes — React Native on the Web

Write once, render anywhere.

In a few minutes minutes, you’ll have a Pokedex app, one that runs on iOS, Android, and on the web.

If you prefer being walked through, check out my video below in which I go though this with you from scratch.

The source code can be found here:

https://github.com/jonnyk20/hybrid-app-pokedex

The phrase “write once, render anywhere” is music to the ears of anyone who’s ever had the tedious task of recreating a web app as a mobile app.

How convenient would it be to write the same React code for both React and React Native. It makes life much easier, and although it has it’s limitations and caveats, it’s a great approach for building multi-platform apps.

Let’s see how it’s done.

Before we Start…

I’m assuming that you:

  • Have a basic understanding of React (understanding React native as well is preferred but not necessary)
  • Have some understanding of ES6
  • Have npm installed on your machine
  • Have yarn installed. If you don’t, feel free to substitute any instance of yarn add ... with npm install -S ....

1) First Mobile

Expo on your Computer

Download the Expo XDE through the above link. Expo will save us the headache of running iOS/Android simulators on our machine. With Expo, you can create a React Native app and test it on your own phone. There is an CLI as well that you could use, but we’ll be using the XDE for this project. Once you’ve done that, open it and register an account if you haven’t already.

Expo on your Phone

Through the link above, find and download Expo client for the mobile device (iOS version, or Android) on which you’ll be testing. Once it’s downloaded, log in using the same account you’re using with theXDE.

Create your app

Open theExpo XDE and click ‘create new project…’ to create an app. Use the blank template.

Run your app

  • Open the expo client on your device
  • Your project should appear under ‘Recently in Development’
  • Click it and wait while the Javacript bundle is built on your phone
  • You should see the following screen on your device
  • Change some text in App.js and save it, and your app should automatically reload with the updated text

Congrats! You now have the setup necessary to build and test a native app, now let’s make it work on a web browser.

2) Then Web

Dependencies

Let’s start by getting the necessary packages to run this on the web.

Open your terminal, navigate to the project folder and then run the following command:

yarn add react-scripts react-dom react-native-web react-art react-router-native react-router-dom

Here’s what we’re adding:

  • react-scripts: contains the scripts used in create-react-app.
  • react-dom: allows react-code to be rendered into an HTML page
  • react-native-web: the main source of magic in this app. This library will convert our react-native components into web elements. Thankfully, react-scripts is already configured to use it, so you won’t need to touch any Webpack yourself to get it up and running
  • react-art: a peer dependency for react-native-web
  • react-router-native: routing library for React Native
  • react-router-dom: routing library for React on the web

File Restructure

We need to have two separate entry points which point to the same root application. One App.js will be used by Expo, and the other src/index.js will be used by react-scripts to be rendered on the web.

  • Create a folder called src and copy the existing App.js into it to create src/App.js
  • Refactor your root folder’s App.js so that it simply imports and renders the src/App.js that you just created
// /App.js
import React from 'react';
import HybridApp from './src/App';
const App = (props) => {
return (
<HybridApp />
);
}
export default App;
  • Create a folder called public and create public/index.html
  • In your index.html simply make a html skeleton, with <div id="root"></div> in the body so that your app has somewhere to render
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Pokedex</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
  • In your src folder, create index.js and write the following code to have it render your app into the DOM
// src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import App from './App';
ReactDom.render(<App />, document.getElementById("root"));

Scripts

react-scripts is automatically configured to recognize React Native code and translate it using the react-native-web library that we imported. That’s why we can make this work without pulling our hair out over Webpack configurations. All we need to do now is set some scripts in our package.json so that we can easily run it.

Add the following property to your package.json

// package.json
"scripts": {
"start-web": "react-scripts start",
"build-web": "react-scripts build"
},

Run it on the Web

In your terminal, run yarn start-web and in moments your web app should appear in the browser. Congratulations, you have created your first hybrid app!

Now let’s make this app do something.

Adding Content and Functionality

Let’s start off simple by adding a list of Pokemon.

To keep things simple, we’ll just store our list of Pokemon in src/spokemonStore.js.

// src/pokemonStore.js
export default [
{
number: '1',
name: 'Bulbasaur',
photoUrl: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png',
type: 'grass'
},
{
number: '4',
name: 'Charmander',
photoUrl: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png',
type: 'fire'
},
{
number: '7',
name: 'Squirtle',
photoUrl: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/007.png',
type: 'water'
}
];

Instead of using map, we’ll follow React Native convention and use FlatList, as it will work just fine with react-native-web.

// src/App.js
import React, { Component } from 'react';
import { View, Text, FlatList, StyleSheet } from 'react-native';
import pokemon from './pokemonStore'
class App extends Component {
render() {
return (
<View style={styles.container}>
<FlatList
keyExtractor={pokemon => pokemon.number}
data={pokemon}
renderItem={({ item }) => <Text>{item.name}</Text>}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
marginTop: 50,
padding: 50
},
});
export default App;

Platform-Specific Styles

Note: It won’t be an issue for a barebones project like ours, but in your own projects, you may have to adjust the styles web layout so that it looks similar to your native app. To do so, you can create src/index.css with your web-specific adjustments and and import it into index.js.

/* src/index.css */
#root {
/* layout adjustments for web app */
}

However, that will effect the layout of that app as a whole. Inevitably we need to figure out away to change specific parts of the app based on the platform, and this is where things get interesting.

Platform-Specific Code

We need to add some basic routing so that we can go from viewing a list of Pokemon to the details of a single Pokemon. react-router-dom is great for the web, and another library, react-router-native, is great for React Native. To keep our code clean, we will create a generic Routing.js file that will abstract that distinction and be available throughout our code.

We will create routing.web.js with components from one library and routing.native.js with similarly named equivalents from the other. Import statements to our routing files will omit the file extension, and because of that, the compiler will automatically import whichever one is relevant to the platform for which the app is being compiled and ignore the other.

This will allow us to avoid writing code that checks for the platform and uses the appropriate library every single time we use routing.

First create the web version of your routing file src/routing.web.js

export {
BrowserRouter as Router,
Switch,
Route,
Link
} from 'react-router-dom';

Then, the native version, src/routing.native.js .

export {
NativeRouter as Router,
Switch,
Route,
Link
} from 'react-router-native';

Now, create two files, Home.js and Pokemon.js . These will be the components rendered by your routes.

In Home.js, simply render a list of Pokemon like you did in App.js.

// src/Home.js
import React from 'react';
import { View, Text, FlatList } from 'react-native';
import pokemon from './pokemonStore';
const Home = props => {
return (
<View>
<FlatList
keyExtractor={pokemon => pokemon.number}
data={pokemon}
renderItem={({ item }) => <Text>{item.name}</Text>}
/>
</View>
);
};
export default Home;

In Pokemon.js, render information about a single Pokemon. You can hard-code in one now as a placeholder. We’ll refactor and tie everything together after we’re sure that the routing works.

// src/Pokemon.js
import React from 'react';
import { View, Text, Image } from 'react-native';
import pokemon from './pokemonStore';
const Pokemon = props => {
const examplePokemon = pokemon[0];
return (
<View>
<View>
<View>
<Text>{`#${examplePokemon.number}`}</Text>
</View>
<View>
<Text>{`Name: ${examplePokemon.name}`}</Text>
</View>
<View>
<Text>{`Type: ${examplePokemon.type}`}</Text>
</View>
<View>
<Image
style={{ width: 50, height: 50 }}
source={{ uri: examplePokemon.photoUrl }}
/>
</View>
</View>
</View>
);
};
export default Pokemon;

Now change your App.js so that it now simply renders your routes. One for each component. Make sure you pass in all of the route’s props into the component by adding {…props}. You’ll need that in order to access the ‘history’ prop which can be used to change routes.

// src/App.js
import React, { Component } from 'react';
import { View, StyleSheet } from 'react-native';
import { Router, Switch, Route } from './routing';
import Home from './Home';
import Pokemon from './Pokemon';
class App extends Component {
render() {
return (
<View style={styles.container}>
<Router>
<Switch>
<Route exact path="/" render={props => <Home {...props} />} />
<Route path="/pokemon" render={props => <Pokemon {...props} />} />
</Switch>
</Router>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
marginTop: 50,
padding: 50
}
});
export default App;

Refresh your app and make sure that each route works by changing the address bar manually. I’m deliberately adding a ‘render’ prop instead of ‘component’ into each route because we’ll be passing in some props to each component as we render them.

Your root route will look the same as before. Type ‘localhost:3000/pokemon’ in your address bar and this what you should get:

Tying it all Together.

Now it’s time to add some logic to make this app work. We need to do the following:

  • In App.js, create a function called selectPokemon which takes a Pokemon as an argument and sets it to the App state as ‘selectedPokemon’. To prevent errors, set a default state with a ‘selectedPokemon’ set to null
  • Pass selectPokemon into Home.js through its props
  • In Home.js Wrap each Pokemon in the list in a <TouchableOpacity /> component so that, once clicked, it can: a) Call selectPokemon and pass in the pokemon as an argument and b) call this.props.history.push(‘/pokemon’) in order to switch to the Pokemon route
  • In App.js, pass in a prop called selectedPokemon into the <Pokemon /> being rendered in the second route. It’s value should be this.state.selectedPokemon, which will be undefined until you select a Pokemon.
  • In the Pokemon component, remove the reference to the pokemonStore and instead refer to props.selectedPokemon. It would be a good idea to also add some default content that is conditionally shown if no Pokemon is selected.
  • Create View and some Text with the message ‘return to home’. To make it work, wrap it with a <Link/> Tag from your routing file and in that Link component, add the property ‘to=”/”’ to have it redirect to your home route.

Here are what your three changed files should now look like:

// src/App.js
import React, { Component } from 'react';
import { View, StyleSheet } from 'react-native';
import { Router, Switch, Route } from './routing';
import Home from './Home';
import Pokemon from './Pokemon';
class App extends Component {
state = {
selectedPokemon: null
};
selectPokemon = selectedPokemon => {
this.setState({
selectedPokemon
});
};
render() {
return (
<View style={styles.container}>
<Router>
<Switch>
<Route
exact
path="/"
render={props => (
<Home {...props} selectPokemon={this.selectPokemon} />
)}
/>
<Route
path="/pokemon"
render={props => (
<Pokemon
{...props}
selectedPokemon={this.state.selectedPokemon}
/>
)}
/>
</Switch>
</Router>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
marginTop: 50,
padding: 50
}
});
export default App;
// src/Home.js
import React from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity
} from 'react-native';
import pokemon from './pokemonStore';
const Home = props => {
const handlePress = pokemon => {
props.selectPokemon(pokemon);
props.history.push('/pokemon');
};
return (
<View>
<FlatList
keyExtractor={pokemon => pokemon.number}
data={pokemon}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => handlePress(item)}>
<Text>{item.name}</Text>
</TouchableOpacity>
)}
/>
</View>
);
};
export default Home;
// src/Pokemon.js
import React from 'react';
import { View, Text, Image } from 'react-native';
import { Link } from './routing';
const Pokemon = props => {
const backButton = (
<View>
<Link to="/">
<Text>Go Back</Text>
</Link>
</View>
);
if (!props.selectedPokemon) {
return (
<View>
{backButton}
<Text>No Pokemon selected</Text>
</View>
);
}
const {
selectedPokemon: { name, number, type, photoUrl }
} = props;
return (
<View>
<View>
{backButton}
<View>
<Text>{`#${number}`}</Text>
</View>
<View>
<Text>{`Name: ${name}`}</Text>
</View>
<View>
<Text>{`Type: ${type}`}</Text>
</View>
<View>
<Image style={{ width: 50, height: 50 }} source={{ uri: photoUrl }} />
</View>
</View>
</View>
);
};
export default Pokemon;

You’re done! you should now be able to load your app, see a list of Pokemon, click on one to see more it’s details, and then press ‘Go back’ to see your list again.

Bonus tip: Platform-Specific code — The ‘Share’ Button

I showed you how to separate code for different platforms by using different files. However, there will likely be times where you have plaftorm-specific logic that can be solved in such a simple way. For example, we’re going to show a ‘share’ button in our Pokemon view, but we only need it on mobile. It would be overkill to make two versions of that entire component just to hide or show one feature. Instead, we’ll simply hide the functionality where it’s not needed. React Native gives us access to a Platform object, which contains a property called ‘OS’ with a value that changes with the operating system of whichever platform your app is running on. We can use that to hide or show our ‘share’ button accordingly.

 // Make sure you import { Platform } from 'react-native';
handlePress = () => {
Share.share({
message: 'Check out my favorite Pokemon!',
url: props.selectePokemon.photoUrl
})
};
...
{ Platform.OS !== 'web' &&
<View>
<Button title="Share" onPress={this.handlePress}/>
</View>
}

Our example is relatively simple, but building a universal React app will inevitably get tricky as you add scale and functionality. Many React Native libraries have little to no compatibility with the web, and even those that do will often require some tinkering with Webpack in order to work. Also keep in mind that Expo, as much as it significantly simplifies setup, adds some limitation to the libraries that you can work with. If you’d like to know details about compatibility with specific libraries, this list would be a great place to start:

https://native.directory/

Learning More

To understand the concept of universal components at a higher level, I recommend this amazing talk by Peggy Rayzis, which is what inspired me to try this in the first place. She also has slides here which provide a great overview of the ecosystem.

Also, from a more practical how-to standpoint, it’s also worth thanking Yannick Spark, whose tutorial mine is partly based on. I merely elaborated on his examples and added Pokemon :). Here’s his tutorial:

https://medium.com/@yannickdot/write-once-run-anywhere-with-create-react-native-app-and-react-native-web-ad40db63eed0

I’m working on a follow-up which will show you how to work with the debugger and redux-dev-tools on a hybrid app, so that you can better understand what’s going on in your app and you don’t have to wait until you see a wall of red text to realize that something might be wrong.

Until then, I hope you enjoyed this and will take a shot at building your own hybrid app. Thanks for reading and feel free to comment or reach out if you have questions or feedback.

Also, check out my Youtube channel and my other Posts for other quick tutorials to help you with personal projects.