Build Mobile-Friendly Web Apps with React Native Web

Build Mobile-Friendly Web Apps with React Native Web

React Native is a multi-platform solution developed by Facebook that lets you build mobile apps using JavaScript. These mobile apps are considered multi-platform because they're written once and deployed across many platforms, like Android, iOS and the web.

React Native is a multi-platform solution developed by Facebook that lets you build mobile apps using JavaScript. These mobile apps are considered multi-platform because they're written once and deployed across many platforms, like Android, iOS and the web.

Over the years, building web applications that are mobile friendly has become easier with the advent of media queries and the introduction of service workers. Using media queries, we could make web applications that different shapes when viewed on mobile devices. Service workers, with their powerful features, present web applications with powers only native applications have been known to possess — push notifications, background sync, etc.

Table of Contents

React Native is a multi-platform solution developed by Facebook that lets you build mobile apps using JavaScript. These mobile apps are considered multi-platform because they’re written once and deployed across many platforms, like Android, iOS and the web. Today, we’ll see how we can utilize this powerful technology to build platform-focused applications on the web.

We’ll be building a simple application that displays user information from a random API using React Native components like ScrollView, Text and Image.

The library we’ll be using to build the demo is called React Native Web, "React Native for Web makes it possible to run React Native components and APIs on the web using React DOM”.

We’ll be building a simple application that will run both on the web and mobile using the React Native Web library, the application displays user information from a random API using React native components like ScrollView, Text, Image etc.

Prerequisites

To follow this tutorial, you’ll need to have Node > 6.0 installed. You’ll also need a package manager NPM (this comes packaged with Node) or Yarn(follow the installation guide here).

Basic knowledge of JavaScript and React is also required. You can follow the official React tutorial here to get up to speed with React.

Getting started

Before we get started, we’ll need to set up the project and install project dependencies. We’ll be making use of Create React App to bootstrap our application. We’re using Create React App because it can be configured to alias React Native. We’ll be installing polyfills for some of the latest JavaScript APIs like Promise, Array.from, etc., as the transpiler doesn’t provide those.

To bootstrap your application using Create React App, run the following command:

    yarn create react-app random-people
              or
    npx create-react-app random-people

Run the following command to install the project’s development dependencies:

    yarn add --dev babel-plugin-module-resolver babel-plugin-transform-object-rest-spread babel-plugin-transform-react-jsx-source babel-preset-expo
              or
    npm install --save-dev babel-plugin-module-resolver babel-plugin-transform-object-rest-spread babel-plugin-transform-react-jsx-source babel-preset-expo

The babel-plugin-module-resolver is a plugin that resolves your project modules when compiling with Babel. We’ll use this package to alias react-native to react-native-web when setting up the project config.

To build and run our application, we’ll be using Expo. Expo is an open-source toolchain built around React Native for building Android and iOS applications. It provides access to the system’s functionality like the Camera, Storage, etc.

Install the expo-cli by running the following command:

    yarn global add expo-cli
            or
    npm i g expo-cli

The next step is to install expo locally, alongside React Native and React Native Web. Run the command below to install the packages:

    yarn add expo react-native react-native-web react-art
            or
    npm i expo react-native react-native-web react-art

After downloading the packages needed to run and build the application, the next step is to setup the configuration files. Create a file called .babelrc in the root of your project and update it with the following:

    //.babelrc{
      "presets": ["babel-preset-expo"],
      "env": {
        "development": {
          "plugins": ["transform-object-rest-spread", "transform-react-jsx-source"]
        }
      },
      "plugins": [
        [
          "module-resolver",
          {
            "alias": {
              "^react-native$": "react-native-web"
            }
          }
        ]
      ]
    }

Create a file named app.json. This file is used to configure parts of your application that don’t belong in the code like the application name, description, sdkVersion, etc. You can find the options available for the app.json file here.

Let’s update the package.json file to include commands for running our application on Android and iOS emulators. Also, we’ll include the main field referencing the App.js file. This file will act as the entry file for the expo-cli. Open the package.json file in a file editor of choice:

    // package.json
    {
      "name": "random-people",
      "version": "0.1.0",
      "private": true,
      "main": "./App.js",
      ...
      "scripts": {
        "start-web": "react-scripts start",
        "build-web": "react-scripts build",
        "test-web": "react-scripts test",
        "eject-web": "react-scripts eject",
        "start-native" : "expo start",
        "android": "expo android",
        "ios": "expo ios",
        "build:ios": "expo build:ios",
        "build:android": "expo build:android",
      },
      ...
    }

Run npm run start-web to run the application and visit http://localhost:3000 to view the application.

Home component

Our application is a simple demo that displays users via the random user API. Using the API, we’ll get display a name and avatar of the returned users through some of the components provided by React Native. Within the src/ directory, create a file named home.js. Open this file using an editor and update it with the snippet below:

    //home.js
    import React from "react";
    import {
      ScrollView,
      ActivityIndicator,
      StyleSheet
    } from "react-native";

    class Home extends React.Component {
      state = {
        users: [],
        loading: true
      };
      componentDidMount() {
        // TODO: get users
      }

      render() {
        return (
          <ScrollView
            noSpacer={true}
            noScroll={true}
            style={styles.container}
          >
           <ActivityIndicator
                style={[styles.centering, styles.gray]}
                color="#ff8179"
                size="large"
              />
          </ScrollView>
        );
      }
    }

    var styles = StyleSheet.create({
      container: {
        backgroundColor: "whitesmoke"
      },
      centering: {
        alignItems: "center",
        justifyContent: "center",
        padding: 8,
        height: '100vh'
      },
    });

    export default Home;

In the snippet above, the Home component renders a ScrollView component that houses the component’s elements. Currently, the component displays an ActivityIndicator; this will be replaced by the user list when the call to the API is complete.

We create styles for the elements using the StyleSheet component. This allows us to style the component using properties similar to CSS properties.

Let’s create a method that gets random users from the Random User API. This method will be called during the componentDidMount lifecycle. Update the home.js component to include the getUsers method:

    // home.js
    import React from 'react';
    ...

    class Home extends React.Component {
      state = {
        ...
      };
      componentDidMount() {
        this.getUsers();
      }

      async getUsers() {
        const res = await fetch("https://randomuser.me/api/?results=20");
        const { results } = await res.json();
        this.setState({ users: [...results], loading: false });
      }
      ...
    }

We can easily make requests using the native Fetch API. Results from the request are parsed and added to state. When the request is complete, we’ll hide the ActivityIncidator by setting loading to false.

App component

The AppComponent holds the logic for the application. We’ll update the default view created by Create React App to suit that of our application by adding logic to display native components.

Create a new file named App.js in the root of your project. This file will be similar to the src/App.js file. The root App.js file will act as the entry file for expo, and the src/App.js file exists for Create React App builds. Update both files with the snippet below:

    import React from 'react';
    import {
      AppRegistry,
      StyleSheet,
      View,
    } from 'react-native';
    import Home from './home'

    class App extends React.Component {
      render() {
        return (
          <View style={styles.appContainer}>
            <Home />
          </View>
        );
      }
    }

    const styles = StyleSheet.create({
      appContainer: {
        flex: 1,
      },
    });

    AppRegistry.registerComponent('App', () => App);

    export default App;

In the snippet above, we register our App component using the AppRegistry. The AppRegistry is the entry point of React Native applications.

Creating user item

Each user item will be displayed using a View component. The View component is an important building block that supports layout using flexbox, styling and accessibility. The View component of each item will be within a SwipeableFlatList. Each item will display the user’s avatar, name and email. Create a file called user-item.js within the src/ folder and update it with the code below:

    // user-item.js

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

    const UserItem = ({ item: user }) => {
      return (
        <View style={styles.row}>
          <Image style={styles.rowIcon} source={user.picture.medium} />
          <View style={styles.rowData}>
            <Text style={styles.rowDataText}>{`${user.name.title} ${
              user.name.first
            } ${user.name.last}`}</Text>
            <Text style={styles.rowDataSubText}>{user.email}</Text>
          </View>
        </View>
      );
    };

    const styles = StyleSheet.create({
      row: {
        flexDirection: "row",
        justifyContent: "center",
        alignItems: "center",
        padding: 15,
        marginBottom: 5,
        backgroundColor: "white",
        borderBottomWidth: StyleSheet.hairlineWidth,
        borderBottomColor: "rgba(0,0,0,0.1)"
      },
      rowIcon: {
        width: 64,
        height: 64,
        marginRight: 20,
        borderRadius: "50%",
        boxShadow: "0 1px 2px 0 rgba(0,0,0,0.1)"
      },
      rowData: {
        flex: 1
      },
      rowDataText: {
        fontSize: 15,
        textTransform: "capitalize",
        color: "#4b4b4b"
      },
      rowDataSubText: {
        fontSize: 13,
        opacity: 0.8,
        color: "#a8a689",
        marginTop: 4
      }
    });

    export default UserItem;

To display the avatar of each user, we make use of the Image component. The component takes a source prop which acts the src which we are used to on the web. The component can be styled further as we have using styles.rowIcon property.

Next, we’ll create the UserList to display each UserItem.

Creating users list

The FlatList component is one that is performant in its list rendering methods. It lazy-loads the items within the list, and only loads more items when the user has scrolled to the bottom of the list. The SwipeableFlatList is a wrapper around the FlatList provided by React Native Web, which makes each item within the list swipeable — so each item will reveals a set of actions when swiped.

Let’s create the SwipeableFlatList for the users returned from the API. Import the SwipeableFlatList component from the react-native package and update the render function to display the list. Create a file called user-list.js and update the file with the following:

    import React from "react";
    import { SwipeableFlatList } from "react-native";
    import UserItem from "./user-item";

    const UserList = ({ users }) => {
      return (
        <SwipeableFlatList
          data={users}
          bounceFirstRowOnMount={true}
          maxSwipeDistance={160}
          renderItem={UserItem}
        />
      );
    };

    export default UserList;

Let’s update the home.js file to include the new UserList. Open the /src/home.js file and update it with the following:

    import React from "react";
    import { ScrollView, StyleSheet, ActivityIndicator } from "react-native";
    import UserList from "./user-list";

    class Home extends React.Component {
      state = {
        users: [],
        loading: true
      };
     ...
      render() {
        return (
          <ScrollView noSpacer={true} noScroll={true} style={styles.container}>
            {this.state.loading ? (
              <ActivityIndicator
                style={[styles.centering]}
                color="#ff8179"
                size="large"
              />
            ) : (
              <UserList users={this.state.users} />
            )}
          </ScrollView>
        );
      }
    }

    const styles = StyleSheet.create({
      ...
    });
    export default Home;

Now if you visit http://localhost:3000 on your browser, you should see a list of users, similar to the screenshot below:

If you can’t see users listed, go through the tutorial again to see what you’ve have missed along the way.

We’re using a SwipeableFlatList component which means each user item is swipeable, so let’s add actions that you can swipe to reveal. What’s the need of a SwipeableFlatList if it doesn’t reveal anything.

Adding actions to item

Each item within the list will be provided a set of actions that will be revealed when swiped to the left. The actions set will use the TouchableHighlight component encompassed by the View component. The TouchableHighlight component is used when we require viewers to respond to touches, more or less acting like a button. Create a file named user-actions.js in the src/ folder. Open the file and copy the following contents into it:

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

    const styles = StyleSheet.create({
      actionsContainer: {
        flex: 1,
        flexDirection: "row",
        justifyContent: "flex-end",
        alignItems: "center",
        padding: 10
      },
      actionButton: {
        padding: 10,
        color: "white",
        borderRadius: 6,
        width: 80,
        backgroundColor: "#808080",
        marginRight: 5,
        marginLeft: 5
      },
      actionButtonDestructive: {
        backgroundColor: "#ff4b21"
      },
      actionButtonText: {
        textAlign: "center"
      }
    });

    const UserActions = () => {
      return (
        <View style={styles.actionsContainer}>
          <TouchableHighlight
            style={styles.actionButton}
            onPress={() => {
              Alert.alert("Tips", "You could do something with this edit action!");
            }}
          >
            <Text style={styles.actionButtonText}>Edit</Text>
          </TouchableHighlight>
          <TouchableHighlight
            style={[styles.actionButton, styles.actionButtonDestructive]}
            onPress={() => {
              Alert.alert(
                "Tips",
                "You could do something with this remove action!"
              );
            }}
          >
            <Text style={styles.actionButtonText}>Remove</Text>
          </TouchableHighlight>
        </View>
      );
    };

    export default UserActions;

The TouchableHighlight component takes an onPress callback that is triggered when the component is clicked. Each callback triggers an Alert display. Styles are also applied to the encompassing View component and other components on the page.

To include the actions on each user item, update the UserList component to include the renderQuickActions prop, which also takes a function.

    import React from "react";
    ...
    import UserActions from "./user-actions";

    const UserList = ({ users }) => {
      return (
        <SwipeableFlatList
          ...
          renderQuickActions={UserActions}
        />
      );
    };

    export default UserList;

Now when you swipe left on any user item it reveals two actions. It should be similar to the screenshot below:

Header component

Now that we’ve successfully fetched users and displayed them using native components, let’s liven the application by setting a header. Using the SafeAreaView component, we’ll create an area with defined boundaries. This will act as the header for our application. Create file called header.js and update it with the code below:

    // src/header.js
    import React from 'react';
    import {SafeAreaView, View, Text, StyleSheet} from 'react-native';

    const Header = ({ onBack, title }) => (
      <SafeAreaView style={styles.headerContainer}>
        <View style={styles.header}>
          <View style={styles.headerCenter}>
            <Text accessibilityRole="heading" aria-level="3" style={styles.title}>{title}</Text>
          </View>
        </View>
      </SafeAreaView>
    );

    const styles = StyleSheet.create({
      headerContainer: {
        borderBottomWidth: StyleSheet.hairlineWidth,
        borderBottomColor: '#ff4e3f',
        backgroundColor: '#ff8179',
      },
      header: {
        padding: 10,
        paddingVertical: 5,
        alignItems: 'center',
        flexDirection: 'row',
        minHeight: 50
      },
      headerCenter: {
        flex: 1,
        order: 2
      },
      headerLeft: {
        order: 1,
        width: 80
      },
      headerRight: {
        order: 3,
        width: 80
      },
      title: {
        fontSize: 19,
        fontWeight: '600',
        textAlign: 'center',
        color: 'white'
      },
    });

    export default Header;

Now let’s add the Header component to the App component. This will display a simple header at the top of the application. Update the App.js file to include the Header component:

    import React from 'react';
    ...
    import Header from './header';

    class App extends React.Component {
      render() {
        return (
          <View style={styles.appContainer}>
            <Header title="Random People" />
            <Home />
          </View>
        );
      }
    }

    const styles = StyleSheet.create({
      ...
    });

    AppRegistry.registerComponent('App', () => App);

    export default App;

After the application refreshes the header will be added to the top of the application.

Looks great, doesn’t it? It won’t be winning any awards, but it might win a lot of prospective react-native-web users. We’ve been able to achieve a lot of functionality after writing minimal code.

Let’s see the various methods we can use to test the application on mobile.

Testing on mobile

To test the application on mobile, the expo-cli provides various method to test the application mobile. The first is using a URL generated after running the application, this URL can be visited on your mobile browser to test the application.

To test the application on mobile, the expo-cli provides various method to test the application mobile. The first is using a URL generated after running the application. This URL can be visited on your mobile browser to test the application.

Run the npm run start-native command within your project folder to run the application using expo. Expo typically starts your application on port 19002, so visit http://localhost:19002 to view the expo dev tools. Within the dev tools you can send a link as an SMS or email to your mobile phone.

You can select any of the three connection options — an external tunnel, LAN or Local connection. For the local connection, your mobile phone and work PC have to be connected to the same network, but the tunnel works regardless.

The next option for testing on a mobile device is using a emulator. Using Android studio or Xcode, you can boot emulators for their respective platforms. Download and install the tool for the platform of choice — Xcode for iOS or Android studio for Android. After installation, run npm run android or npm run ios to start the application on any of the emulators.

Deploying the application

We’ll be deploying our application to the Android Play store. To achieve this, we’ll need to update the app.json to include Android specific properties. Open the app.json file and update the file to include the android field:

    {
      "expo": {
        "sdkVersion": "31.0.0",
        "name": "random-people",
        "slug": "random-people",
        "version": "0.1.0",
        "description": "An application for displaying random people",
        "primaryColor": "#ff8179",
        "android": {
          "package": "com.random.people"
        }
      }
    }

The android.package field is a unique value that will represent your package in the app store. You can read more on the package-naming convention here. After updating the file, run the npm run build:android command.

This command will present you with a prompt, asking you to provide a keystore or to generate a new one. If you have an existing keystore, you can select this option or let expo generate one for your application.

After completion, a download link will be generated for your application. Clicking on this link will trigger a download for your APK.

To deploy the downloaded APKto the Android Play Store, visit the Play Console to create an account. After creating an account, you’ll be required to pay a registration fee of $25 before proceeding. After completing the registration process, visit this page and follow the steps to upload your application to the Play Store.

Summary

Using the React Native Web and React Native libraries, we’ve been able to create an application that can be deployed on several platforms using native components. Building multi-platform applications has never been easier. You can view the source code for the demo here.

React Native Web Full App Tutorial - Build a Workout App for iOS, Android, and Web

Learn to use React Native for Web to create a workout app that works on Android, iOS, and the web. The app uses Mobx, Typescript, React Navigation, React Hooks, AsyncStorage / Localstorage, and more. Once the app is complete, you will learn how to deploy it to Netlify.


💻 Code: https://github.com/benawad/react-native-web-series

⭐️ Contents ⭐️

⌨️ (0:00:00) Setting Up a React Native Web Project

⌨️ (0:09:04) Setting Up React Native Hooks

⌨️ (0:15:03) Setting Up a React Native Web Monorepo

⌨️ (0:28:24) Configuring React Native for Yarn Workspaces

⌨️ (0:39:58) How to use Mobx with React Hooks

⌨️ (0:49:15) Navigation in React Native Web

⌨️ (1:03:28) Styling a Component in React Native Web

⌨️ (1:27:40) Mobx Root Store

⌨️ (1:45:20) Workout Timer with Mobx

⌨️ (2:11:20) React Router with React Native Web

⌨️ (2:22:45) How to Persist Mobx Stores

⌨️ (2:30:47) Storing and Displaying Workout History

⌨️ (2:54:33) React Router Params in React Native Web

⌨️ (3:17:30) Floating Action Button React Native Web

⌨️ (3:34:45) Deploy React Native Web to Netlify

⭐️ Links: ⭐️

🔗 https://stronglifts.com/apps/

🔗 https://github.com/necolas/react-native-web/blob/master/docs/guides/client-side-rendering.md

🔗 VSCode settings used: https://gist.github.com/benawad/1e9dd01994f78489306fbfd6f7b01cd3


⭐️ Learn More ⭐️

React - The Complete Guide (incl Hooks, React Router, Redux)

Modern React with Redux [2019 Update]

React Native - The Practical Guide

Which should I choose: React Native or Flutter?

React Tutorial: Building and Securing Your First App

Build Progressive Web Apps with React

Build a React Calendar Component from scratch

React Native – The Future of Mobile

The Difference Between iOS And Android App Development Using React Native

In this <a href="https://www.cmarix.com/the-difference-between-ios-and-android-app-development-using-react-native/?utm_source=SB" target="_blank">blog</a>, learn how the in-app features from design elements to testing tools, React Native development for iOS and Android mobile app platforms offer several key differences that developers need to know about.

In this blog, learn how the in-app features from design elements to testing tools, React Native development for iOS and Android mobile app platforms offer several key differences that developers need to know about.

Create a todo app React Native for iOS, Android and web

Create a todo app React Native for iOS, Android and web

In this tutorial, I will be describing how to create a todo app with React Native for iOS, Android and web.

You will need Node and Yarn installed on your machine.

The application will be a Todo app but will also make use of Pusher Channels for realtime functionality. You can find a demo of the application below:

In the results of Stack Overflow’s 2019 developer survey, JavaScript happens to be the most popular technology. This is not by mere luck as it has proven we can write applications that can run almost anywhere - from web apps, desktop apps, android apps and iOS apps.

Prerequisites Directory setup

You will need to create a new directory called realtime-todo. In this directory, you will also need to create another one called server. You can make use of the command below to achieve the above:

    $ mkdir realtime-todo
    $ mkdir realtime-todo/server
Building the server

As you already know, we created a server directory, you will need to cd into that directory as that is where the bulk of the work for this section is going to be in. The first thing you need to do is to create a package.json file, you can make use of the following command:

$ touch package.json

In the newly created file, paste the following content:

    // realtime-todo/server/package.json
    {
      "name": "server",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "dependencies": {
        "body-parser": "^1.18.3",
        "cors": "^2.8.5",
        "dotenv": "^7.0.0",
        "express": "^4.16.4",
        "pusher": "^2.2.0"
      },
      "devDependencies": {},
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      }
    }

After which you will need to actually install the dependencies, that can be done with:

$ yarn

Once the above command succeeds, you will need to create an index.js file that will house the actual todo API. You can create the file by running the command below:

$ touch index.js

In the index.js, paste the following contents:

    // realtime-todo/server/index.js

    require('dotenv').config({ path: 'variable.env' });

    const express = require('express');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const Pusher = require('pusher');

    const pusher = new Pusher({
      appId: process.env.PUSHER_APP_ID,
      key: process.env.PUSHER_APP_KEY,
      secret: process.env.PUSHER_APP_SECRET,
      cluster: process.env.PUSHER_APP_CLUSTER,
      useTLS: true,
    });

    const app = express();

    app.use(cors());
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));

    app.post('/pusher/auth', function(req, res) {
      var socketId = req.body.socket_id;
      var channel = req.body.channel_name;
      var auth = pusher.authenticate(socketId, channel);
      res.send(auth);
    });

    const todos = [];

    app.get('/items', (req, res) => {
      res.status(200).send({ tasks: todos });
    });

    app.post('/items', (req, res) => {
      const title = req.body.title;

      if (title === undefined) {
        res
          .status(400)
          .send({ message: 'Please provide your todo item', status: false });
        return;
      }

      if (title.length <= 5) {
        res.status(400).send({
          message: 'Todo item should be more than 5 characters',
          status: false,
        });
        return;
      }

      const index = todos.findIndex(element => {
        return element.text === title.trim();
      });

      if (index >= 0) {
        res
          .status(400)
          .send({ message: 'TODO item already exists', status: false });
        return;
      }

      const item = {
        text: title.trim(),
        completed: false,
      };

      todos.push(item);

      pusher.trigger('todo', 'items', item);

      res
        .status(200)
        .send({ message: 'TODO item was successfully created', status: true });
    });

    app.post('/items/complete', (req, res) => {
      const idx = req.body.index;

      todos[idx].completed = true;

      pusher.trigger('todo', 'complete', { index: idx });

      res.status(200).send({
        status: true,
      });
    });

    app.set('port', process.env.PORT || 5200);
    const server = app.listen(app.get('port'), () => {
      console.log(`Express running on port ${server.address().port}`);
    });

In the above, we create an API server that has three endpoints:

  • /items : an HTTP GET request to list all available todo items.
  • /items : an HTTP POST request to create a new todo item.
  • /items/complete: used to mark a todo item as done.

Another thing you might have noticed in on Line 3 where we make mention of a file called variable.env. That file does not exists yet, so now is the time to create it. You can do that with the following command:

$ touch variable.env

In the newly created file, paste the following content:

    // realtime-todo/server/variable.env

    PUSHER_APP_ID="PUSHER_APP_ID"
    PUSHER_APP_KEY="PUSHER_APP_KEY"
    PUSHER_APP_SECRET="PUSHER_APP_SECRET"
    PUSHER_APP_CLUSTER="PUSHER_APP_CLUSTER"
    PUSHER_APP_SECURE="1"

Please make sure to replace the placeholders with your original credentials

You can go ahead to run the server to make sure everything is fine. You can do that by running the command:

$ node index.js
Building the client

The client we will build in this section will run on the web. With the help of Expo and React Native, it will also run on Android and iOS. This is made possible via a library called [react-native-web]([https://github.com/necolas/react-native-web)](https://github.com/necolas/react-native-web)).

To get up to speed, we will make use of a starter pack available on GitHub. You will need to navigate to the project root i.e realtime-todo and clone the starter pack project. That can be done with the following command:

    # Clone into the `client` directory
    $ git clone [email protected]:joefazz/react-native-web-starter.git client

You will need to cd into the client directory as all changes to be made will be done there. You will also need to install the dependencies, that can be done by running yarn. As we will be making use of Pusher Channels and at the same time communicate with the server, you will need to run the following command:

$ yarn add axios pusher-js

The next step is to open the file located at src/App.js. You will need to delete all the existing content and replace with the following:

    // realtime-todo/client/src/App.js

    import React, { Component } from 'react';
    import {
      StyleSheet,
      Text,
      View,
      FlatList,
      Button,
      TextInput,
      SafeAreaView,
    } from 'react-native';
    import axios from 'axios';
    import Alert from './Alert';
    import Pusher from 'pusher-js/react-native';

    const APP_KEY = 'PUSHER_APP_KEY';
    const APP_CLUSTER = 'PUSHER_APP_CLUSTER';

    export default class App extends Component {
      state = {
        tasks: [],
        text: '',
        initiator: false,
      };

      changeTextHandler = text => {
        this.setState({ text: text });
      };

      addTask = () => {
        if (this.state.text.length <= 5) {
          Alert('Todo item cannot be less than 5 characters');
          return;
        }

        // The server is the actual source of truth. Notify it of a new entry so it can
        // add it to a database and publish to other available channels.
        axios
          .post('http://localhost:5200/items', { title: this.state.text })
          .then(res => {
            if (res.data.status) {
              this.setState(prevState => {
                const item = {
                  text: prevState.text,
                  completed: false,
                };

                return {
                  tasks: [...prevState.tasks, item],
                  text: '',
                  initiator: true,
                };
              });

              return;
            }

            Alert('Could not add TODO item');
          })
          .catch(err => {
            let msg = err;

            if (err.response) {
              msg = err.response.data.message;
            }

            Alert(msg);
          });
      };

      markComplete = i => {
        // As other devices need to know once an item is marked as done.
        // The server needs to be informed so other available devices can be kept in sync
        axios
          .post('http://localhost:5200/items/complete', { index: i })
          .then(res => {
            if (res.data.status) {
              this.setState(prevState => {
                prevState.tasks[i].completed = true;
                return { tasks: [...prevState.tasks] };
              });
            }
          });
      };

      componentDidMount() {
        // Fetch a list of todo items once the app starts up.
        axios.get('http://localhost:5200/items', {}).then(res => {
          this.setState({
            tasks: res.data.tasks || [],
            text: '',
          });
        });

        const socket = new Pusher(APP_KEY, {
          cluster: APP_CLUSTER,
        });

        const channel = socket.subscribe('todo');

        // Listen to the items channel for new todo entries.
        // The server publishes to this channel whenever a new entry is created.
        channel.bind('items', data => {
          // Since the app is going to be realtime, we don't want the same item to
          // be shown twice. Device A publishes an entry, all other devices including itself
          // receives the entry, so act like a basic filter
          if (!this.state.initiator) {
            this.setState(prevState => {
              return { tasks: [...prevState.tasks, data] };
            });
          } else {
            this.setState({
              initiator: false,
            });
          }
        });

        // This "complete" channel here is for items that were recently marked as done.
        channel.bind('complete', data => {
          if (!this.state.initiator) {
            this.setState(prevState => {
              prevState.tasks[data.index].completed = true;
              return { tasks: [...prevState.tasks] };
            });
          } else {
            this.setState({
              initiator: false,
            });
          }
        });
      }

      render() {
        return (
          // SafeAreaView is meant for the X family of iPhones.
          <SafeAreaView style={{ flex: 1, backgroundColor: '#F5FCFF' }}>
            <View style={[styles.container]}>
              <FlatList
                style={styles.list}
                data={this.state.tasks}
                keyExtractor={(item, index) => index.toString()}
                renderItem={({ item, index }) => (
                  <View>
                    <View style={styles.listItemCont}>
                      <Text
                        style={[
                          styles.listItem,
                          item.completed && { textDecorationLine: 'line-through' },
                        ]}
                      >
                        {item.text}
                      </Text>
                      {!item.completed && (
                        <Button
                          title="✔"
                          onPress={() => this.markComplete(index)}
                        />
                      )}
                    </View>
                    <View style={styles.hr} />
                  </View>
                )}
              />

              <TextInput
                style={styles.textInput}
                onChangeText={this.changeTextHandler}
                onSubmitEditing={this.addTask}
                value={this.state.text}
                placeholder="Add Tasks"
                returnKeyType="done"
                returnKeyLabel="done"
              />
            </View>
          </SafeAreaView>
        );
      }
    }

    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
        paddingTop: 20,
        height: '100%',
      },
      list: {
        width: '100%',
      },
      listItem: {
        paddingTop: 2,
        paddingBottom: 2,
        fontSize: 18,
      },
      hr: {
        height: 1,
        backgroundColor: 'gray',
      },
      listItemCont: {
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-between',
      },
      textInput: {
        height: 40,
        paddingRight: 10,
        borderColor: 'gray',
        width: '100%',
      },
    });

Please update Line 17 and 18 to contain your actual credentials.

While the above is pretty straight forward, perhaps the most interesting is the line that reads Alert('Could not add TODO item');. It is easy to think Alert.alert() should be used, while that is true, react-native-web doesn’t include support for the Alert component so we will have to roll out our own. Here is a list of all components react-native-web supports. Building functionality for making alerts on the web isn’t a herculean task. You will need to create a new file called Alert.js in the src directory.

$ touch src/Alert.js

In the newly created file Alert.js, paste the following contents:

    // realtime-todo/client/src/Alert.js

    import { Platform, Alert as NativeAlert } from 'react-native';

    const Alert = msg => {
      if (Platform.OS === 'web') {
        alert(msg);
        return;
      }

      NativeAlert.alert(msg);
    };

    export default Alert;

Simple right ? We just check what platform the code is being executed on and take relevant action.

With that done, you will need to go back to the client directory. This is where you get to run the client. Depending on the platform you want to run the app in, the command to run will be different:

  • Web : yarn web. You will need to visit [http://localhost:3000](http://localhost:3000).
  • Android/iOS : yarn start-expo

If you go with the second option, you will be shown a web page that looks like the following:

You can then click on the links on the left based on your choice.

Remember to leave the server running

If you open the project on the web and on iOS/Android, you will be able to reproduce the demo below:

Conclusion

In this tutorial, I have described how to build an application that runs on Android, iOS and the web with just one codebase. We also integrated Pusher Channels so as to make communication realtime.

As always, you can find the code on GitHub.

Thanks for reading

If you liked this post, share it with all of your programming buddies!

Follow us on Facebook | Twitter

Learn More

The Complete React Native and Redux Course

React Native - The Practical Guide

The complete React Native course ( 2nd edition )

React Native Web Full App Tutorial - Build a Workout App for iOS, Android, and Web

How to build a Chat App with React Native

React Native on Windows