React-Native-Web Tutorials: Your First Hybrid App
Before we start React-Native-Web
- Basic understanding of React (understanding React native as well is preferred but not necessary)
- Some understanding of ES6
- Have npm installed on your machine
- yarn install. If you don’t, feel free to substitute any instance of
yarn add ...
withnpm install -S ....
Downloading Expo
Download the Expo XDE through the above link. Expo will save us the headache of running iOS/Android simulators on our machines. With Expo, you can create a React Native app and test it on your own phone. There is a 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, find and download the 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 the XDE.
Create your app
Open the Expo 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 Javascript 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
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 existingApp.js
into it to createsrc/App.js
- Refactor your root folder’s
App.js
so that it simply imports and renders thesrc/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 createpublic/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, createindex.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 simply by adding a list of Pokemon.
To keep things simple, we’ll just store our list of Pokemon insrc/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 a 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 import them into index.js
.
/* src/index.css */
#root {
/* layout adjustments for web app */
}
However, that will affect the layout of that app as a whole. Inevitably we need to figure out a way 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
the file that will abstract that distinction and be available throughout our code.
We will create routing.web.js
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 as 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. Therefore, deliberately add 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 is 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 calledselectPokemon
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
intoHome.js
through its props - In Home.js Wrap each Pokemon in the list in a
<TouchableOpacity />
component so that, once clicked, it can: a) CallselectPokemon
and pass in the Pokemon as an argument and b) callthis.props.history.push(‘/pokemon’)
in order to switch to the Pokemon route - In
App.js
, pass in a prop calledselectedPokemon
into the <Pokemon /> being rendered in the second route. Its value should bethis.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! and should now be able to load your app, see a list of Pokemon, click on one to see more of its 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 when you have platform-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>
}
As our example is relatively simple, but building a universal React app will inevitably get tricky as you add scale and functionality, since 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. And you should also keep in mind that Expo, as much as it significantly simplifies setup, adds some limitations 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: