React Native For Web: A Glimpse Into The Future
One of the hardest decisions to make when starting a new app is which platforms to target. A mobile app gives you more control and better performance but isn’t as universal as the web. If you’re making a mobile app, can you afford to support both iOS and Android? What about trying to build a mobile app and a responsive web app? Ultimately, the best experience for your customers is for your app to work everywhere, but the development and maintenance costs of that can be prohibitive.
We have already seen how React Native can help you make iOS and Android apps with a shared code base, without sacrifices in quality. But what about the web? This is exactly the problem the React Native for Web project is trying to solve. Instead of forcing you to maintain two separate code bases for your mobile and web apps, or making a hybrid app, with all its compromises.
React Native for Web is intended to let you write a single app that runs in a browser using standard web technologies, or on iOS and Android as a real native mobile app. While I don’t think the project is ready for production use yet, its potential success could mark a massive change in how large multi-platform applications are built. Let’s jump in!
How It Works
You might be thinking, “Wait! doesn’t React already work on the web?” You wouldn’t be wrong. Unfortunately, traditional React and React Native build on a different set of primitives. React uses <div>
, <p>
and <input>
, whereas React Native uses <View>
, <Text>
and <TextInput>
. There are good historical reasons for this, since the building blocks of a web page and of a mobile app are quite different. Nonetheless, it would be great if we could use a single set of shared components.
React Native for Web’s solution is to provide browser-compatible implementations of React Native’s components — meaning, for example, that the <View>
of React Native has a DOM-based version that knows how to render to a <div>
. While not every React Native component is supported, enough of them are that you could (hopefully) share the majority of your code base.
In addition to the components themselves, styles for React and React Native are written differently. With React, most people use plain CSS or a preprocessor such as Sass. But in React Native, all styles are written in JavaScript, because there is no DOM and no selectors. With React Native for Web, styles are written just like they would be for React Native, rather than with CSS. This has the benefit of allowing you to write a single set of styles, which will work on both native mobile and the web.
We’ll take a deeper look later at how these ideas work in practice and at how much code is actually reusable. First, let’s get a sample app going.
Starting A New React Native Project
To get started, we will need to set up our project. At first, this will just be a regular React Native app, and then we’ll add React Native for Web. If you are following along, you’ll need to complete React Native’s “Getting Started” guide before heading into the next section.
Once you’ve got React Native installed, you can run the following command from your terminal:
react-native init ReactNativeWeb
This will make a new React Native project named ReactNativeWeb
. After it has finished installing, you can cd ReactNativeWeb
, and then react-native run-ios
or react-native run-android
. If everything has gone correctly, you should see a friendly welcome message on your iOS or Android simulator or device.
Notice that React Native has created two JavaScript files in our project’s directory: index.android.js
and index.ios.js
. You can edit any of the styles or logic in these files and see those changes update in the running app. As you can probably guess, the .android.js
file is for Android, and the .ios.js
file is for iOS. Fortunately, separate files are only needed when you want multiple versions of a given file per platform. Most of the time, you’ll have a single file per component.
Managing Dependencies
Before we can get our app running in a web browser, we’ll need to get a bit of package installation out of the way. First, run the following to install both the react-native-web
package and the official React web packages.
npm i react react-dom react-native-web --save
(You might see some errors about peer dependencies from this command. You should be safe to ignore them, because they didn’t cause me any problems. If newer versions of any of these packages are out when you run the commands, though, you might need to adjust the installed versions.)
At this point, your package.json
file should look something like this:
{
"name": "ReactNativeWeb",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"react": "15.1.0",
"react-dom": "15.1.0",
"react-native": "0.28.0",
"react-native-web": "0.0.25"
}
}
While we have what seems to be everything required for our React Native app to run in a web browser, we must take a brief detour to consider the realities of web development. React Native’s packager compiles your ECMAScript 6 code to something that a phone’s JavaScript engine can understand, but it won’t help us in the browser. If we tried to run our app in a web browser right now, it would quickly fail due to syntax errors.
To solve this problem, we will use Babel and webpack. Babel will compile our ECMAScript 6 code into browser-compatible ECMAScript 5, and webpack will bundle the compiled JavaScript, as well as just generally make development faster. (There are other options for this. If you prefer another compiler or bundler, feel free to use it instead.)
Here are the installation commands to run:
npm i webpack babel-loader babel-preset-react babel-preset-es2015 --save
npm i webpack-dev-server --save-dev
Here, babel-loader
and webpack-dev-server
will be used to bundle and serve our JavaScript, while babel-preset-react
and babel-preset-es2015
tell Babel which plugins we need to compile our code.
Here is what your package.json
file should look like now:
{
"name": "ReactNativeWeb",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
},
"dependencies": {
"babel-loader": "6.2.4",
"babel-preset-es2015": "6.9.0",
"babel-preset-react": "6.5.0",
"react": "15.1.0",
"react-dom": "15.1.0",
"react-native": "0.28.0",
"react-native-web": "0.0.25",
"webpack": "1.13.1"
},
"devDependencies": {
"webpack-dev-server": "1.14.1"
}
}
Configuring
Those are all of the packages we will need. But more setup is required before our app will work in a browser.
webpack.config.js
First, we’ll make a webpack config
file. This file tells webpack how to build, bundle and serve our compiled code. In addition, we are going to use the alias
property to automatically replace imports on react-native
with react-native-web
. This file should be placed in your project’s root.
const webpack = require('webpack');
module.exports = {
entry: {
main: './index.web.js',
},
module: {
loaders: [
{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['es2015', 'react'],
},
},
],
},
resolve: {
alias: {
'react-native': 'react-native-web',
},
},
};
index.html
Now, we need to create an HTML file for our app to run in. This will be pretty simple because it will just be a skeleton to attach our React app to.
<!DOCTYPE html>
<html>
<head>
<title>React Native Web</title>
<meta charSet="utf-8" />
<meta content="initial-scale=1,width=device-width" name="viewport" />
</head>
<body>
<div id="react-app"></div>
<script type="text/javascript" src="/bundle.js"></script>
</body>
</html>
index.web.js
Finally, we must make an index
JavaScript file for the web. The contents of this file can be the same as index.ios.js
or index.android.js
, but with one additional line to attach to the DOM. The div with the ID react-app
from our HTML file must be selected and then used in the call to AppRegister.runApplication
.
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
class ReactNativeWeb extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit index.web.js
</Text>
<Text style={styles.instructions}>
Press Cmd+R to reload
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
AppRegistry.registerComponent('ReactNativeWeb', () => ReactNativeWeb);
AppRegistry.runApplication('ReactNativeWeb', { rootTag: document.getElementById('react-app') });
Now, just run ./node_modules/.bin/webpack-dev-server –inline
to start webpack, and open your browser to https://localhost:8080/. Fingers crossed, you will see a familiar welcome message but in the browser!
With all of that setup complete, we are ready to start tinkering!
Experimenting With The Code
Create A FriendsList.js Component
Let’s start by making a friends list. This will be a good simple stress test of React Native for Web, because we need to use a few different components for it: <Image>
, <Text>
, <View>
and <ListView>
.
import React, { Component } from 'react';
import {
Image,
ListView,
StyleSheet,
Text,
View,
} from 'react-native';
const styles = StyleSheet.create({
list: {
marginTop: 20,
},
friend: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
},
avatar: {
margin: 10,
width: 50,
height: 50,
borderRadius: 25,
},
name: {
fontSize: 18,
color: '#000',
}
});
export default class FriendsList extends Component {
constructor(props) {
super(props);
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.state = {
ds: ds.cloneWithRows(props.friends),
};
}
render() {
return (
<ListView
dataSource={this.state.ds}
style={styles.list}
renderRow={(friend) =>
<View style={styles.friend}>
<Image style={styles.avatar} source={{ uri: friend.avatarUrl }} />
<Text style={styles.name}>{friend.firstName} {friend.lastName}</Text>
</View>
} />
);
}
}
We’ll need to edit our index
files, too, so that a friends
array gets passed in as a prop.
import FriendsList from './FriendsList';
import React, { Component } from 'react';
import {
AppRegistry,
Text,
View
} from 'react-native';
const friends = [
{
id: 1,
firstName: 'Jane',
lastName: 'Miller',
avatarUrl: 'https://placehold.it/100x100',
},
{
id: 2,
firstName: 'Kate',
lastName: 'Smith',
avatarUrl: 'https://placehold.it/100x100',
},
{
id: 3,
firstName: 'Kevin',
lastName: 'Yang',
avatarUrl: 'https://placehold.it/100x100',
},
];
class ReactNativeWeb extends Component {
render() {
return <FriendsList friends={friends} />;
}
}
AppRegistry.registerComponent('ReactNativeWeb', () => ReactNativeWeb);
Upon running it in iOS or Android, you should see something like this:
Looks good so far. Let’s see the web version:
Uh oh! Turns out there isn’t any web support yet for ListView
’s DataSource
, effectively making ListView
completely unusable.
Friend.js
We can work around this lack of support for now. Let’s make a Friend
component for the individual rows, but have a FriendsList
component per platform. This will separate out the shared code that works everywhere but allow us to customize each platform where we need to.
import React, { Component } from 'react';
import {
Image,
StyleSheet,
Text,
View,
} from 'react-native';
const styles = StyleSheet.create({
friend: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
},
avatar: {
margin: 10,
width: 50,
height: 50,
borderRadius: 25,
},
name: {
fontSize: 18,
color: '#000',
}
});
export default class Friend extends Component {
render() {
return (
<View style={styles.friend}>
<Image style={styles.avatar} source={{ uri: this.props.avatarUrl }} />
<Text style={styles.name}>{this.props.firstName} {this.props.lastName}</Text>
</View>
);
}
}
FriendsList.ios.js
import Friend from './Friend';
import React, { Component } from 'react';
import {
Image,
ListView,
StyleSheet,
Text,
View,
} from 'react-native';
const styles = StyleSheet.create({
list: {
marginTop: 20,
},
});
export default class FriendsList extends Component {
constructor(props) {
super(props);
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
this.state = {
ds: ds.cloneWithRows(props.friends),
};
}
render() {
return (
<ListView
dataSource={this.state.ds}
style={styles.list}
renderRow={(friend) =>
<Friend
key={friend.id}
avatarUrl={friend.avatarUrl}
firstName={friend.firstName}
lastName={friend.lastName} />
} />
);
}
}
On iOS, our ListView
usage code is unchanged. (I’m leaving out the Android code example here and for the rest of the article, for brevity. The Android and iOS code can be the same for the rest of the code samples.)
FriendsList.web.js
import Friend from './Friend';
import React, { Component } from 'react';
import {
Image,
Text,
View,
} from 'react-native';
export default class FriendsList extends Component {
render() {
return (
<View>
{this.props.friends.map(friend =>
<Friend
key={friend.id}
avatarUrl={friend.avatarUrl}
firstName={friend.firstName}
lastName={friend.lastName} />
)}
</View>
);
}
}
Now, for the web, we use the map
function to render each Friend
, similar to traditional React.
Much better. At this point, hearing that ListView
requires workarounds might be enough to make you think that React Native for Web isn’t ready for production use. I am inclined to agree, particularly since lists constitute a large percentage of many applications. How much it matters will vary depending on the project, though. On the bright side, all of our other React Native code so far has been completely reusable. In any case, I am still interested in exploring it further, because there is still much potential in the ideas on display here. Let’s continue with our sample app.
Instead of hardcoding a handful of list items, we can use JSON Generator to create a long list for us to work with. If you haven’t used it before, JSON Generator is a great tool for creating dummy and development data. Here is the structure I have defined, which adds a few fields on top of what we already have.
[
'{{repeat(200)}}',
{
id: '{{guid()}}',
firstName: '{{firstName()}}',
lastName: '{{surname()}}',
avatarUrl: 'https://placehold.it/100x100',
isOnline: '{{bool()}}',
company: '{{company()}}',
email: '{{email()}}'
}
]
And here is a snippet of some of the generated data:
[
{
"id": "c5368bbe-adfb-424f-ade3-9d783befa2b6",
"firstName": "Hahn",
"lastName": "Rojas",
"avatarUrl": "https://placehold.it/100x100",
"isOnline": true,
"company": "Orbixtar",
"email": "hahnrojas@orbixtar.com"
},
{
"id": "15ef2834-3ba5-4621-abf1-d771d39c2dd6",
"firstName": "Helen",
"lastName": "Stout",
"avatarUrl": "https://placehold.it/100x100",
"isOnline": true,
"company": "Ebidco",
"email": "helenstout@ebidco.com"
},
{
"id": "1ef05de1-fd8e-41ae-85ac-620b6d716b62",
"firstName": "Floyd",
"lastName": "Mcpherson",
"avatarUrl": "https://placehold.it/100x100",
"isOnline": false,
"company": "Ecraze",
"email": "floydmcpherson@ecraze.com"
},
…
]
To use it, just take your generated JSON and replace our friends
array declaration from before. Of course, you can move that data into its own file if you’d like, so that your code files aren’t cluttered with data. In a real application, we would get that data from an API server.
Friend.js
Next, we can add these new fields to the Friend
component.
…
render() {
return (
<View style={styles.friend}>
<Image
style={[styles.avatar, { borderColor: this.props.isOnline ? '#9d9' : '#d99' }]}
source={{ uri: this.props.avatarUrl }} />
<View>
<Text style={styles.name}>{this.props.firstName} {this.props.lastName}</Text>
<Text style={styles.company}>{this.props.company}</Text>
<Text style={styles.email}>{this.props.email}</Text>
</View>
</View>
);
}
…
FriendsList.js
Next, add them as props in each platform’s FriendsList
.
…
<Friend
key={friend.id}
avatarUrl={friend.avatarUrl}
firstName={friend.firstName}
lastName={friend.lastName}
isOnline={friend.isOnline}
company={friend.company}
email={friend.email} />
…
So far so good. It is encouraging to see that the core components seem to work well.
Friend.js
Next, we can add an animation with a transformation to see how well those work. Let’s make it so that when you tap a row, it animates left and right before returning to its initial position. We will need to add imports for Animated
and TouchableOpacity
, and wire up the animation and press handler.
import {
Animated,
TouchableOpacity,
…
} from 'react-native';
…
export default class Friend extends Component {
constructor(props) {
super(props);
this.state = {
translateValue: new Animated.Value(0),
};
}
animate() {
Animated.sequence([
Animated.timing(this.state.translateValue, {
toValue: 50,
duration: 200,
}),
Animated.timing(this.state.translateValue, {
toValue: -50,
duration: 200,
}),
Animated.timing(this.state.translateValue, {
toValue: 0,
duration: 200,
})
]).start();
}
render() {
return (
<TouchableOpacity onPress={() => this.animate()} style={[styles.friend, { transform: [{ translateX: this.state.translateValue }]}]}>
<Image
style={[styles.avatar, { borderColor: this.props.isOnline ? '#9d9' : '#d99' }]}
source={{ uri: this.props.avatarUrl }} />
<View>
<Text style={styles.name}>{this.props.firstName} {this.props.lastName}</Text>
<Text style={styles.company}>{this.props.company}</Text>
<Text style={styles.email}>{this.props.email}</Text>
</View>
</TouchableOpacity>
);
}
}
Looks good on mobile.
What about the web?
No luck. Our TouchableOpacity
throws an error when pressed. Apparently, this will be fixed in the next release and is only present for our particular combination of versions. Attempting to run the animation without using TouchableOpacity
causes the same error, too.
I am going to stop here, but if you want to continue on your own, here is a list of topics you could research next:
- How well do the remaining React Native components and APIs work? We’ve seen that some definitely don’t work, but we don’t yet have a comprehensive list of support.
- You could explore more extensive styling work, including media queries.
- React Native for Web supports server rendering. This could be particularly cool because, if it works, it would mean that you could have a single code base driving native mobile applications and a responsive web app that is SEO-optimized.
Conclusion
As you can tell, React Native for Web is definitely not ready for production. There are too many unsupported components, even in our small demo app, for me to feel confident about using it in a real project. The most encouraging thing to me, though, is that the pieces that do work seem to completely work, and the parts that don’t, fail entirely. I find that much preferable to the entire thing just kind of working. At the moment, it seems like the project just needs more time to build support. If everything was only 50% functional, I would view that as a sign that the approach is fundamentally broken.
Despite the problems, I still think this is a very exciting project and worth keeping an eye on.
Resources
- React Native for Web, GitHub
- “Getting Started,” React Native
Further Reading
- Why You Should Consider React Native For Your Mobile App
- How To Scale React Applications
- Building Your First iOS App With JavaScript
- Internationalizing React Apps