1 Comment

Running a Static HTTP Server in React Native

One of my recent projects required us to make an existing web application work offline. Initially, we considered making a progressive web app. We quickly eliminated this option since PWAs have several limitations, most importantly storage size.

Eventually, we decided to embed our web application into a WebView within a React Native application since we could take advantage of the device’s file system and keep re-implementation to a minimum. In order to do this, we needed a way to host our web application from within React Native.

We landed on the library react-native-static-server for hosting our web app for two reasons. First, we only needed to serve static assets. And secondly, it worked out of the box.

This post will focus on serving HTML, JavaScript, and CSS using react-native-static-server, along with viewing that content in a WebView in both Android and iOS.

1. Setup

For the sake of this tutorial, we are going to start with a fresh React Native project, so let’s run react-native init <ProjectName> && cd <ProjectName>.

Next, install the necessary project dependencies with the following commands:


## NOTE: At the time of writing this article, the latest release of react-native-static-server's release process is broken. The repo at this commit fixes that issue
yarn add git://github.com/futurepress/react-native-static-server.git#390303423ef1d2185d64eff246532bdc8b57fb33
yarn add react-native-webview
react-native link react-native-static-server
react-native link react-native-webview
cd ios && pod install && cd -

2. Running the Server

Once your dependencies are installed, put the following code into your App.js file:


import React from "react";
import { SafeAreaView, Text, View } from "react-native";
import StaticServer from "react-native-static-server";
import WebView from "react-native-webview";

class App extends React.Component {
  state = {
    url: null
  };
  componentDidMount() {
    this.server = new StaticServer(8080);
    this.server.start().then(url => {
      this.setState({ url });
    });
  }

  componentWillUnmount() {
    if (this.server && this.server.isRunning()) {
      this.server.stop();
    }
  }

  render() {
    if (!this.state.url) {
      return (
        <SafeAreaView>
          <Text>Hello World</Text>
        </SafeAreaView>
      );
    }
    return (
      <SafeAreaView>
        <Text>{this.state.url}</Text>
        <View style={{ height: "100%", width: "100%" }}>
          <WebView
            style={{ flex: 1, marginBottom: 20 }}
            source={{ uri: this.state.url }}
          />
        </View>
      </SafeAreaView>
    );
  }
}
export default App;

This code does a few things:

  1. It creates a static server that points at the document directory by default. (Note: This code will only work in iOS since the library does not contain a default directory for Android.)
  2. It starts the server and sets the URL of the server’s endpoint in the components state.
  3. It waits until the URL is set in the state to render a WebView that points at the local server.
  4. It kills the web server after this component un-mounts.

3. Displaying Content

If you run this code, you will notice that no content is being displayed.

This is because the server does not have any content to serve. In order to change this, we need to package our app with our desired assets.

First, let’s add a directory named assets/www to our project. We’ll insert the following files:

index.html:


<html>
  <head>
    <link rel="stylesheet" type="text/css" href="index.css" />
  </head>
  <body>
    <p id="text">Hello World!</p>
    <div id="newStuff"></div>
    <script type="text/javascript" src="./index.js"></script>
  </body>
</html>

index.css:


#text {
  color: red;
}

index.js:


document.getElementById("newStuff").innerHTML = "The javascript works?";

These files will help us prove two things: that our device is capable of serving HTML, and that any files referenced in the HTML will also be served.

4. Addding Files to Native Platform

Next, we need to add these files to each individual Native platform.

iOS

In order to add these assets to iOS:

  1. Open your project in Xcode by opening up ios/<ProjectName>.xcodeproj.
  2. Right-click the top-level directory of your project and select ‘Add Files to “<ProjectName>”’.
  3. In the base of your project directory, select the assets/www/ directory.

Your assets are now bundled in your iOS project! Your Xcode project directory should look something like this:

Android

To add your assets to your Android project, add the following code to your app/build.gradle file:


android {
...
    sourceSets { main { assets.srcDirs = ['src/main/assets', '../../assets'] } }
}

That’s it! If you open up your android directory in Android Studio and view your directory in Android mode, it should look something like this:

After you have linked your assets, we need to add react-native-fs so we can move our files and get the directories where our content is located. To do this, run the following commands:

yarn add react-native-fs
react-native link react-native-fs

Now, add the following code above your App component in App.js


function getPath() {
  return Platform.OS === "android"
    ? RNFS.DocumentDirectoryPath + "/www"
    : RNFS.MainBundlePath + "/www";
}

async function moveAndroidFiles() {
  if (Platform.OS === "android") {
    await RNFS.mkdir(RNFS.DocumentDirectoryPath + "/www");
    const files = ["www/index.html", "www/index.css", "www/index.js"];
    await files.forEach(async file => {
      await RNFS.copyFileAssets(file, RNFS.DocumentDirectoryPath + "/" + file);
    });
  }
}

And change your componentDidMount method to this:


  async componentDidMount() {
    moveAndroidFiles();
    let path = getPath();
    this.server = new StaticServer(8080, path);
    this.server.start().then(url => {
      this.setState({ url });
    });
  }

This change is required for two reasons:

  1. Our server needs a directory to serve content.
  2. react-native-static-server doesn’t let you serve assets from the asset directory.
  3. Android doesn’t have a MainBundle directory, so it requires a different path.

We’re all done! If you run the project, you should see something like this:

If you would like to see this code in its complete form, check out the code here.