Build and Deploy a React Native Web App on Netlify from Scratch

December 18, 2019

React Native Web allows developers to write web apps using React Native components and patterns. Unlike regular React components, components written with React Native Web can easily be shared across other platforms that React Native supports, like iOS, Android, and Windows. While tools like Expo Web can make getting started a breeze, the configuration and setup is simple enough that anyone can easily build a React Native Web project from scratch. This article will show exactly how easy it is to setup, build, and deploy a React Native Web app by building the site www.howtoexitvim.org from scratch without any template.

Github - Website

  • Setup and Configuration
  • Hello World with React Native Web
  • Building howtoexitvim.org
  • Deploying to Netlify

Setup and Configuration

We’re going to setup everything from scratch with the minimum configuration required.

Create a new directory, howtoexitvim.

mkdir howtoexitvim

Initialize a package.json and change the main entry point to src/index.js.

npm init

npm init

React Native Web does not require any more dependencies than a regular React app, aside from the react-native-web package itself. Components from react-native-web are built with React DOM, so we do not need to install React Native itself for projects that only support web browsers.

We will use Babel to transform our code and Webpack to both serve and bundle the app. Install the following dependencies:

npm i react react-dom react-native-web webpack webpack-cli webpack-dev-server html-webpack-plugin html-loader babel-loader @babel/core @babel/preset-env @babel/preset-react

Next, create webpack.config.js at the root of the project with the usual configuration for a React app. Check out this excellent article, How to configure React with Webpack & Friends from the ground up by Mark A, to learn how each of these sections work.

const HtmlWebPackPlugin = require("html-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules\/(?!()\/).*/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env", "@babel/preset-react"],
          },
        },
      },
      {
        test: /\.html$/,
        use: [
          {
            loader: "html-loader",
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebPackPlugin({
      template: "./public/index.html",
      filename: "./index.html",
    }),
  ],

  devServer: {
    historyApiFallback: true,
    contentBase: "./",
    hot: true,
  },
};

Let’s also alias react-native to react-native-web so that when Webpack sees this:

import { SomeComponent } from 'react-native'

It will instead import the component from react-native-web, like this:

import { SomeComponent } from 'react-native-web'

This saves us the hassle of changing the imports if we use our code on mobile. Add the following between plugins and devServer.

...
resolve: {
    alias: {
      "react-native": "react-native-web"
    },
    extensions: [".web.js", ".js"]
  },
...

Finally, create an npm script to run the webpack-dev-server.

...
"scripts": {
    "start": "webpack-dev-server --config ./webpack.config.js --mode development",
  },
...

Now that all of the dependencies and configuration is setup, let’s create a simple hello world app with React Native Web.

Hello World with React Native Web

When you are done with this section, your folder structure should look like this.

hello-files

First, create a new folder public to hold all the static files in the app. Then create a barebones index.html file within that folder.

<!DOCTYPE html>
<html>
  <head>
    <title>How To Exit Vim</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

Next, create src/App.js with the text “Hello World” using React Native’s <Text> component.

import React from "react";
import ReactDOM from "react-dom";
import { Text } from "react-native";

export default class App extends React.Component {
  render() {
    return <Text>Hello World</Text>;
  }
}

The last file we’ll need is src/index.js, which will render the app in the DOM using react-dom.

import React from "react";
import ReactDOM from "react-dom";

import App from "./App";

ReactDOM.render(<App />, document.getElementById("app"));

Finally, run npm start in the terminal to run the app. Visit http://localhost:8080/ to see the the “Hello World”.

Hello World

Building howtoexitvim.org

Essentially, howtoexitvim.org will display a few commands to exit vim with a short description of what the command does. To accomplish this we will only need four components: Container, Title, Escape, and Command. However, before we start building the React Native components, we need to import the fonts faces for the title and content, as well as set the height of body to 100% so our background will naturally fill the whole page.

Adding Fonts and Height 100%

Add the following between the <head> tags in public/index.html:

...
<style>
  @import "https://fonts.googleapis.com/css?family=Orbitron";
  @import "https://fonts.googleapis.com/css?family=Monoton";

  body,
  #app {
    height: 100%;
    background-color: black;
  }
</style>

Container

The Container will set the background and position the content in the center of the page. For the background we’ll pick one of the linear gradients on www.gradientmagic.com.

// src/Container.js

import React from "react";
import PropTypes from "prop-types";
import { View, Text } from "react-native";

export default function Container({ children }) {
  return (
    <View style={styles.container}>
      <View style={styles.content}>{children}</View>
    </View>
  );
}

const styles = {
  container: {
    backgroundColor: "black",
    backgroundImage:
      "repeating-linear-gradient(0deg, hsla(103,11%,32%,0.09) 0px, hsla(103,11%,32%,0.09) 1px,transparent 1px, transparent 11px),repeating-linear-gradient(90deg, hsla(103,11%,32%,0.09) 0px, hsla(103,11%,32%,0.09) 1px,transparent 1px, transparent 11px),linear-gradient(90deg, hsl(317,13%,6%),hsl(317,13%,6%))",

    height: "100%",
    minHeight: "100vh",
    padding: 24,
    justifyContent: "center",
    alignItems: "center",
  },
  content: {
    maxWidth: 785,
  },
};

Container.propTypes = {
  children: PropTypes.node,
};

Import the Container component and wrap the Text component in src/App.js to see the new background.

// src/App.js
...
import Container from "./Container";
...
...
<Container>
  <Text>Hello World</Text>
</Container>
...

Title

title

The Title will render the page’s title in the awesome Monoton font. We can make this title stand out even more by adding a text shadow to create a glow effect.

import React from "react";
import PropTypes from "prop-types";
import { View, Text } from "react-native";

export default function Title({ title }) {
  return <Text style={styles}>{title}</Text>;
}

const styles = {
  fontSize: 70,
  fontFamily: "Monoton",
  color: "#FF00DE",
  letterSpacing: 8,
  textShadowColor: "#FF00DE",
  textShadowOffset: { width: -1, height: 1 },
  textShadowRadius: 30,
  marginBottom: 16,
  textAlign: "center",
};

Title.propTypes = {
  title: PropTypes.string,
};

Import Title component and replace the Text component in src/App.js.

// src/App.js
...
<Container>
  <Title title={"How to Exit Vim"} />
</Container>
...

Escape

The Escape component will display the information: “Hit Esc first”, since you need to exit edit mode before running any of the commands to quit VIM. We’re going to style this text in a similar way to the title, using text shadows to create a glow effect. But we’re going to use the font Orbitron instead of Monoton, since it’s more easily readable as text. Also, we need to distinguish between text that is describing what to do and text the visitor should type on their keyboard. We’ll make this distinction with both font size and color. Description text will be 30px and #7fff00, while command text will be 40px and #7DF9FF.

// src/Escape.js

import React from "react";
import { View, Text } from "react-native";

export default function Escape() {
  return (
    <View style={styles.container}>
      <Text style={styles.description}>
        Hit <Text style={styles.command}>Esc</Text> first
      </Text>
    </View>
  );
}

const styles = {
  container: {
    flexDirection: "row",
    justifyContent: "center",
    marginBottom: 24,
  },
  command: {
    fontSize: 40,
    color: "#7DF9FF",
    textShadowColor: "#7DF9FF",

    fontFamily: "Orbitron",

    textShadowOffset: { width: -2, height: 2 },
    textShadowRadius: 30,
  },
  description: {
    fontSize: 30,
    color: "#7fff00",
    textShadowColor: "#7fff00",
    fontFamily: "Orbitron",

    textShadowOffset: { width: -1, height: 1 },
    textShadowRadius: 30,
  },
};

Import the Escape component and add it below the Title in src/App.js.

// src/App.js
...
<Container>
  <Title title={"How to Exit Vim"} />
  <Escape />
</Container>
...

Command

The last component, Command, will display the keyboard command on the left and the description of what the command does on the right. The description will also have a subDescription that elaborates on what the command does. The text styles will match the styles we defined in the Escape component to keep the distinction between commands and descriptions.

// src/Command.js

import React from "react";
import PropTypes from "prop-types";
import { View, Text } from "react-native";

export default function Command({ description, command, subDescription }) {
  return (
    <View style={styles.container}>
      <Text style={styles.command}>{command}</Text>
      <View style={styles.descriptionContainer}>
        <Text style={styles.description}>{description}</Text>
        {subDescription ? (
          <Text style={styles.subDescription}>({subDescription})</Text>
        ) : null}
      </View>
    </View>
  );
}

const styles = {
  container: {
    flexDirection: "row",
    justifyContent: "space-between",
    marginBottom: 30,
  },

  command: {
    fontSize: 40,
    color: "#7DF9FF",
    textShadowColor: "#7DF9FF",
    fontFamily: "Orbitron",
    textShadowOffset: { width: -2, height: 2 },
    textShadowRadius: 30,
    flex: 1,
    marginRight: 8,
  },
  descriptionContainer: {
    flex: 1,
  },
  description: {
    fontSize: 18,
    color: "#7fff00",
    textShadowColor: "#7fff00",
    fontFamily: "Orbitron",
    textShadowOffset: { width: -1, height: 1 },
    textShadowRadius: 30,
    textAlign: "right",
    marginBottom: 6,
  },
  subDescription: {
    fontSize: 12,
    color: "#59af03",
    textShadowColor: "#59af03",
    fontFamily: "Orbitron",
    textShadowOffset: { width: -1, height: 1 },
    textShadowRadius: 30,
    textAlign: "right",
  },
};

Command.propTypes = {
  description: PropTypes.string,
  command: PropTypes.string,
};

Import the Command component into src/App.js and add some commands to exit vim.

// src/App.js
...
<Container>
  <Title title={"How to Exit Vim"} />
  <Escape />
  <View>
    <Command
      description={"Quit"}
      subDescription={"Fails if changes were made"}
      command={":q"}
    />
    <Command
      description={"Quit without writing"}
      subDescription={"Discard changes"}
      command={":q!"}
    />

    <Command
      description={"Write current file and Quit"}
      subDescription={"Saves changes even if there aren't any"}
      command={":wq"}
    />
    <Command
      description={"Write current file and Quit"}
      subDescription={"Saves changes only if there are changes"}
      command={":x"}
    />
    <Command
      description={"Quit without writing"}
      subDescription={"Discard changes"}
      command={"shift + ZQ"}
    />
    <Command
      description={"Write current file and Quit"}
      subDescription={"Saves changes only if there are changes"}
      command={"shift + ZZ"}
    />
  </View>
</Container>
...

We should now have a complete app displaying a few commands to exit VIM. The last step is to deploy it to Netlify.

Cover

Deploying React Native Web to Netlify

Netlify is a hosting provider that enables developers to host static websites. We can host our React Native Web app on Netlify by creating a static bundle of our app and assets using Webpack’s production mode. Add the following as an npm script, named “build”, to package.json.

...
"scripts": {
    "build": "webpack --mode production",
    "start": "webpack-dev-server --config ./webpack.config.js --mode development",
},
...

Running this command in the terminal should output the app as static files index.html and main.js.

npm run build

Although we could upload these files directly to Netlify, it would be better to automate this process so that the project deploys when the master branch is updated on Github.

Automated Builds on Netlify

Sign in or create a Netlify account, then go to Sites and click the “New site from Git” button.

Netlify

Then click your Git provider and follow the instructions provided to connect it to Netlify.

Git Provider

Follow the onscreen prompts to choose the git repo where the app is stored. On the third step, choose the branch to deploy as “master”. Fill in the build command as npm run build, and the publish directory as dist. Finally, click the “Deploy Site” button at the bottom.

Build Settings

The app should start deploying with a dash separated randomly generated name.

Domain

The app should now be live at that address within in the Netlify subdomain, for example elegant-wescoff-754899.netlify.com.

Conclusion

Building websites with React Native Web is extremely similar to building websites with raw React. The only significant difference in this project compared to an identical project using raw React is that all the div and p tags were replaced with View and Text components. This is small price to pay for having the option of supporting more platforms in the future without a significant rewrite, even if your project does not support multiple platforms. Having said that, the example above is extremely simple, more complex applications might need to pay a higher price with limitations or components that are difficult to write with React Native. But even with that higher price, the value of sharing code across so many platforms is, in my opinion, worth it.