react-native-web: Run your react native app on browser

React native web: Write once, render everywhere

 

React since its inception has gained popularity amongst developers and has completely changed the way web applications were developed. React has gained a vast majority of its fame because of Virtual DOM. Virtual DOM is not something restricted to React World, but other UI frameworks also have been built around the concept of the Virtual DOM-like, Vue.JS. The main reason of React’s immense popularity is because it was developed by Facebook.

After leaving its mark on the web world, Facebook launched React native to conquer over the ever-growing mobile world. With React Native into the picture, we can develop mobile apps using Javascript which builds native applications which can easily run on Android as well as iOS.

But the write-once runs anywhere philosophy of React Native is restricted to the mobile world only.

React Native Web turns your react native mobile application into a web application without even writing a single line of code and it is pretty simple and straightforward to do it.

Before we Start

Make sure you have the followings:

  • 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) Mobile app

Install expo

What is expo: A free and open source toolchain built around React Native to help you build native iOS and Android apps using JavaScript and React. In short, expo helps in building react-native apps faster.

https://github.com/expo/xde/releases

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 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.

https://expo.io/tools#client

Create your app:

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

Create 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

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) 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 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 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 it 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 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. Its 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 is 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 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 where 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>
}

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/

 

Our Services

© All rights reserved BITCS

Made by BITCS​​

Translate »