Technology25+ minute read

Full-stack NLP With React: Ionic vs. Cordova vs. React Native

JavaScript frameworks based on React can help you build a fast, reliable mobile app, but it’s not always easy to determine which framework is best for your project. Choosing the wrong framework can result in an app with slow and redundant code.

JavaScript expert Sean Wang builds the same natural language processing mobile application using Cordova, Ionic, and React Native, then discusses the advantages and limitations of each.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

JavaScript frameworks based on React can help you build a fast, reliable mobile app, but it’s not always easy to determine which framework is best for your project. Choosing the wrong framework can result in an app with slow and redundant code.

JavaScript expert Sean Wang builds the same natural language processing mobile application using Cordova, Ionic, and React Native, then discusses the advantages and limitations of each.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Shanglun Wang
Verified Expert in Engineering

Sean is a passionate polyglot: a full-stack wizard, sysadmin, and data scientist. He’s also developed market intelligence software.

PREVIOUSLY AT

CB Insights
Share

In the approximately 15 years since Apple released the first iPhone, the software development landscape has changed dramatically. As smartphones gain widespread adoption and continue to grow in unique capabilities, users increasingly prefer to access software services through mobile devices rather than desktops or laptops. Smartphones offer features such as geolocation, biometric authentication, and motion sensing, many of which desktop platforms are only now starting to copy. In some demographics, a smartphone or a similar mobile device is the primary means of software consumption, bypassing computers altogether.

Companies have noticed this shift and reinforced it in major ways. Mobile apps are no longer an afterthought. Applications ranging from Robinhood, a financial brokerage company, to Instagram, a social media company, to Uber, a ride-hailing company, are adopting a mobile-first development strategy. If there is a desktop application, it is often offered as a complement to the mobile app, rather than the primary focus.

For full-stack developers, adapting to these shifting trends is crucial. Fortunately, there are many mature and well-supported technologies available to help web developers apply their skills to mobile development. Today, we will explore three such technologies: Cordova, Ionic, and React Native. We will be using React.js, one of the most popular frameworks for front-end web development, as our core development technology. While we will be focusing on developing an iPhone application, these are cross-platform technologies and will be cross-compilable to the Android platform.

What We Will Build Today

We will build an application that uses Natural Language Processing (NLP) to process and curate Twitter feeds. The application will allow the user to select a set of Twitter handles, pull the most recent updates using a Twitter API, and categorize the tweets based on sentiment and topic. The user will then be able to view the tweets based on sentiment or topic.

Back End

Before we build the front end, we will want to build the back end. We will keep the back end simple for now - we’ll use basic, off-the-shelf sentiment analysis and part-of-speech tagging, along with a little bit of data cleaning to deal with dataset-specific problems. We will use an open-source NLP library called TextBlob and serve the result over Flask.

Sentiment Analysis, Part-of-speech Tagging, and NLP: A Quick Primer

If you have not worked with natural language analysis applications before, these terms may be very foreign to you. NLP is an umbrella term for technologies that analyze and process natural human language data. While this is a broad topic, there are many challenges that are common to all technologies that tackle this area. For example, human language, unlike programming language or numerical data, tends to be loosely structured due to the permissive nature of human language grammar. Additionally, human language tends to be extremely contextual, and a phrase uttered or written in one context may not translate to another context. Finally, structure and context aside, language is extremely complex. Words further down in a paragraph could change the meaning of a sentence at the beginning of the paragraph. Vocabulary can be invented, redefined, or changed. All of these complexities make data analysis techniques difficult to cross-apply.

Sentiment Analysis is a subfield of NLP that focuses on understanding the emotionality of a natural language passage. While human emotion is inherently subjective, and therefore difficult to pin down technologically, sentiment analysis is a subfield that has immense commercial promise. Some applications of sentiment analysis include classifying product reviews to identify positive and negative assessment of various features, detecting the mood of an email or a speech and grouping song lyrics by mood. If you’re looking for a more in-depth explanation of Sentiment Analysis, you can read my article on building a Sentiment Analysis-based application here.

Part-of-speech tagging, or POS tagging, is a very different subfield. The goal of POS tagging is to identify the part of speech of a given word in a sentence using grammatical and contextual information. Identifying this relationship is a much more difficult task than initially meets the eye - a word can have very different parts of speech based on the context and the structure of the sentence, and the rules aren’t always clear even to humans. Fortunately, many off-the-shelf models today provide powerful and versatile models integrated with most major programming languages. If you would like to learn more, you can read my article on POS tagging here.

Flask, TextBlob, and Tweepy

For our NLP back end, we will use Flask, TextBlob, and Tweepy. We will use Flask to build a small, lightweight server, TextBlob to run our natural language processing, and Tweepy to get tweets from the Twitter API. Before you start coding, you will also want to get a developer key from Twitter so you can retrieve tweets.

We can write a much more sophisticated back end and use more complex NLP technologies, but for our purposes today, we will keep the back end as simple as possible.

Back-end Code

Now, we’re ready to start coding. Fire up your favorite Python editor and terminal, and let’s get cracking!

First, we will want to install the requisite packages.

pip install flask flask-cors textblob tweepy
python -m textblob.download_corpora

Now, let’s write the code for our functionality.

Open up a new Python script, call it server.py, and import the requisite libraries:

import tweepy

from textblob import TextBlob
from collections import defaultdict

Let’s now write some helper functions:

# simple, average a list of numbers with a guard clause to avoid division by zero
def mean(lst):
    return sum(lst)/len(lst) if len(lst) > 0 else 0

# call the textblob sentiment analysis API and noun phrases API and return it as a dict
def get_sentiment_and_np(sentence):
    blob = TextBlob(sentence)
    return{
        'sentiment': mean([s.sentiment.polarity for s in blob.sentences if s.sentiment.polarity != 0.0]),
        'noun_phrases': list(blob.noun_phrases)
    }

# use the tweepy API to get the last 50 posts from a user’s timeline
# We will want to get the full text if the text is truncated, and we will also remove retweets since they’re not tweets by that particular account.
def get_tweets(handle):
    auth = tweepy.OAuthHandler('YOUR_DEVELOPER_KEY')
    auth.set_access_token('YOUR_DEVELOPER_SECRET_KEY')
    api = tweepy.API(auth)
    tl = api.user_timeline(handle, count=50)
    tweets = []
    for tl_item in tl:
        if 'retweeted_status' in tl_item._json:
            Continue # this is a retweet
        if tl_item._json['truncated']:
            status = api.get_status(tl_item._json['id'], tweet_mode='extended') # get full text
            tweets.append(status._json['full_text'])
        else:
            tweets.append(tl_item._json['text'])
    return tweets

# http and https are sometimes recognized as noun phrases, so we filter it out.
# We also try to skip noun phrases with very short words to avoid certain false positives
# If this were a commercial app, we would want a more sophisticated filtering strategy.
def good_noun_phrase(noun_phrase):
    noun_phrase_list = noun_phrase.split(' ')
    for np in noun_phrase_list:
        if np in {'http', 'https'} or len(np) < 3:
            return False
    return True

Now that we have the helper functions written, we can put everything together with a couple of simple functions:

# reshapes the tagged tweets into dictionaries that can be easily consumed by the front-end app
def group_tweets(processed_tweets):
    # Sort it by sentiment
    sentiment_sorted = sorted(processed_tweets, key=lambda x: x['data']['sentiment'])

    # collect tweets by noun phrases. One tweet can be present in the list of more than one noun phrase, obviously.
    tweets_by_np = defaultdict(list)

    for pt in processed_tweets:
        for np in pt['data']['noun_phrases']:
            tweets_by_np[np].append(pt)
    grouped_by_np = {np.title(): tweets for np, tweets in tweets_by_np.items() if len(tweets) > 1 and good_noun_phrase(np)}
    return sentiment_sorted, grouped_by_np

# download, filter, and analyze the tweets
def download_analyze_tweets(accounts):
    processed_tweets = []
    for account in accounts:
        for tweet in get_tweets(account):
            processed_tweet = ' '.join([i for i in tweet.split(' ') if not i.startswith('@')])
            res = get_sentiment_and_np(processed_tweet)
            processed_tweets.append({
                'account': account,
                'tweet': tweet,
                'data': res
            })

    sentiment_sorted, grouped_by_np = group_tweets(processed_tweets)
    return processed_tweets, sentiment_sorted, grouped_by_np

You can now run the function download_analyze_tweets on a list of handles that you want to follow, and you should see the results.

I ran the following code:

if __name__ == '__main__':
    accounts = ['@spacex', '@nasa']
    processed_tweets, sentiment_sorted, grouped_by_np = download_analyze_tweets(accounts)
    print(processed_tweets)
    print(sentiment_sorted)
    print(grouped_by_np)

Executing this produced the following results. The results are obviously time-dependent, so if you see something similar, you’re on the right track.

[{'account': '@spacex', 'tweet': 'Falcon 9…
[{'account': '@nasa', 'tweet': 'Our Mars rove…
{'Falcon': [{'account': '@spacex', 'tweet': 'Falc….

Now we can build out the Flask server, which is quite simple. Create an empty file called server.py and write the following code:

from flask import Flask, request, jsonify
from twitter import download_analyze_tweets
from flask_cors import CORS

app = Flask(__name__)

CORS(app)

@app.route('/get_tweets', methods=['POST'])

def get_tweets():
    accounts = request.json['accounts']
    processed_tweets, sentiment_sorted, grouped_by_np = download_analyze_tweets(accounts)
    return jsonify({
        'processedTweets': processed_tweets,
        'sentimentSorted': sentiment_sorted,
        'groupedByNp': grouped_by_np
    })

if __name__ == '__main__':
    app.run(debug=True)

Run the server, and you should now be able to send a post request to the server using an HTTP client of your choice. Pass in {accounts: [“@NASA”, “@SpaceX”]} as a json argument, and you should see the API return something similar to what was returned in the Twitter analysis code.

Now that we have a server, we are ready to code the front end. Because of a small nuance in networking over a phone emulator, I recommend that you deploy your API somewhere. Otherwise, you will want to detect whether your app is running on an emulator and send requests to <Your Computer IP>:5000 instead of localhost:5000 when it is in an emulator. If you deploy the code, you can simply issue the request to that URL.

There are many options to deploy the server. For a free, simple debug server with minimal setup required, I recommend something like PythonAnywhere, which should be able to run this server out of the box.

Now that we have coded the back-end server, let’s look at the front end. We’ll start with one of the most convenient options for a web developer: Cordova.

Apache Cordova Implementation

Cordova Primer

Apache Cordova is a software technology to help web developers target mobile platforms. By taking advantage of web browser capabilities implemented on smartphone platforms, Cordova wraps web application code into a native application container to create an application. Cordova isn’t simply a fancy web browser, however. Through the Cordova API, web developers can access many smartphone-specific features such as offline support, location services, and on-device camera.

For our application, we will write an application using React.js as the JS framework and React-Bootstrap as the CSS framework. Because Bootstrap is a responsive CSS framework, it already has support for running on smaller screens. Once the application is written, we will compile it into a web application using Cordova.

Configuring the App

We will start by doing something unique to set up the Cordova React app. In a Medium article, developer Shubham Patil explains what we’re doing. Essentially, we’re setting up a React development environment using the React CLI, and then a Cordova development environment using the Cordova CLI, before finally merging the two.

To start, run the following two commands in your code folder:

cordova create TwitterCurationCordova

create-react-app twittercurationreact

Once the setup is done, we will want to move the contents of the React app’s public and src folder to the Cordova app. Then, in the package.json, copy over the scripts, browser list, and dependencies from the React project. Also add "homepage": "./" at the root of the package.json to enable compatibility with Cordova.

Once the package.json is merged, we will want to change the public/index.html file to work with Cordova. Open up the file and copy the meta tags from www/index.html as well as the script at the end of the body tag when Cordova.js is loaded.

Next, change the src/index.js file to detect if it’s running on Cordova. If it’s running on Cordova, we will want to run the render code within the deviceready event handler. If it’s running in a regular browser, just render right away.

const renderReactDom = () => {
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
}

if (window.cordova) {
  document.addEventListener('deviceready', () => {
    renderReactDom();
  }, false);
} else {
  renderReactDom();
}

Finally, we need to set up our deployment pipeline. Add the below definition into the config.xml file:

<hook type="before_prepare" src="hooks/prebuild.js" />

And put the following script into prebuild.js:

const path = require('path');
const { exec } = require('child_process');
const fs = require('fs');
const rimraf = require('rimraf');

function renameOutputFolder(buildFolderPath, outputFolderPath) {
    return new Promise((resolve, reject) => {
        fs.rename(buildFolderPath, outputFolderPath, (err) => {
            if (err) {
                reject(err);
            } else {
                resolve('Successfully built!');
            }
        });
    });
}

function execPostReactBuild(buildFolderPath, outputFolderPath) {
    return new Promise((resolve, reject) => {
        if (fs.existsSync(buildFolderPath)) {
            if (fs.existsSync(outputFolderPath)) {
                rimraf(outputFolderPath, (err) => {
                    if (err) {
                        reject(err);
                        return;
                    }
                    renameOutputFolder(buildFolderPath, outputFolderPath)
                        .then(val => resolve(val))
                        .catch(e => reject(e));
                });
            } else {
                renameOutputFolder(buildFolderPath, outputFolderPath)
                    .then(val => resolve(val))
                    .catch(e => reject(e));
            }
        } else {
            reject(new Error('build folder does not exist'));
        }
    });
}

module.exports = () => {
    const projectPath = path.resolve(process.cwd(), './node_modules/.bin/react-scripts');
    return new Promise((resolve, reject) => {
        exec(`${projectPath} build`,
            (error) => {
                if (error) {
                    console.error(error);
                    reject(error);
                    return;
                }
                execPostReactBuild(path.resolve(__dirname, '../build/'), path.join(__dirname, '../www/'))
                    .then((s) => {
                        console.log(s);
                        resolve(s);
                    })
                    .catch((e) => {
                     console.error(e);
                        reject(e);
                    });
            });
    });
};

This executes the React build and puts the build folder into the appropriate place before the Cordova build starts, thus automating the deployment process.

Now, we can try running our app. Run the following in the command line:

npm install rimraf
npm install
npm run start

You should see the React app set up and running in the browser. Add Cordova now:

cordova platform add iOS

cordova run iOS

And you should see the React app running in the emulator.

Router and Packages Setup

To set up some of the infrastructure that we need to build the app, let’s start by installing the requisite packages:

npm install react-bootstrap react-router react-router-dom

We will now set up the routing, and while doing so, we will also set up a simple global state object that will be shared by all the components. In a production application, we will want to use a state management system like Redux or MobX, but we will keep it simple for now. Go to App.js and configure the route thus:

import {
  BrowserRouter as Router,
  Redirect,
  Route,
} from "react-router-dom";

function App() {
  const [curatedTweets, setCuratedTweets] = useState();
  return <Router>
      <Route path="/" exact render={() => <Input setCuratedTweets={setCuratedTweets} />} />
      <Route path="/display" render={() => <Display curatedTweets={curatedTweets} />} />
      <Route path="*" exact render={() => <Redirect to="/" />} />
  </Router>
}

With this route definition, we have introduced two routes that we will need to implement: Input and Display. Notice that the curatedTweets variable is being passed to Display, and the setCuratedTweets variable is being passed to Input. This means the input component will be able to call the function to set the curatedTweets variable, and Display will get the variable to display.

To start coding the components, let’s create a folder under /src called /src/components. Under /src/components, create another folder called /src/components/input and two files underneath: input.js and input.css. Do the same for the Display component - create /src/components/display and underneath: display.js and display.css.

Under those, let’s create stub components, like so:

import React from ‘react’;
import ‘input.css’

const Input = () => <div>Input</div>;
export default Input

And the same for Display:

import React from ‘react’;
import display.css’

const Display = () => <div>Display</div>;
export default Display

With that, our wireframing is complete and the app should run. Let us now code up the Input page.

Input Page

Big-picture Plan

Before we write the code, let’s think about what we want our Input page to do. Obviously, we will want a way for the users to input and edit the Twitter handles that they want to pull from. We also want the users to be able to indicate that they’re done. When the users indicate they are done, we will want to pull the curated tweets from our Python curation API and finally navigate to the Display component.

Now that we know what we want our component to do, we are ready to code.

Setting Up the File

Let’s start by importing React Router library’s withRouter to get access to navigation functionality, the React Bootstrap Components we need, like so:

import React, {useState} from 'react';
import {withRouter} from 'react-router-dom';
import {ListGroup, Button, Form, Container, Row, Col} from 'react-bootstrap';
import './input.css';

Now, let’s define the stub function for Input. We know that Input gets the setCuratedTweets function, and we also want to give it the ability to navigate to the display route after it sets the curated tweets from our Python API. Therefore, we will want to take from the props setCuratedTweets and history (for navigation).

const Input = ({setCuratedTweets, history}) => {
    return <div>Input</div>
}

To give it the history API access, we will wrap it with withRouter in the export statement at the end of the file:

export default withRouter(Input);

Data Containers

Let’s set up the data containers using React Hooks. We already imported the useState hook so we can add the following code to the Input component’s body:

const [handles, setHandles] = useState([]);
const [handleText, setHandleText] = useState(‘’);

This creates the container and modifiers for handles, which will hold the list of handles that the user wishes to pull from, and the handleText, which will hold the content of the textbox that the user uses to input the handle.

Now, let’s code up the UI components.

UI Components

The UI components will be fairly simple. We will have one Bootstrap row that contains the input textbox along with two buttons, one to add the current input box content to the list of handles, and one to pull from the API. We will have another Bootstrap row that displays the list of handles that the user wishes to pull using the Bootstrap list group. In code, it looks like so:

return (
    <Container className="tl-container">
        <Row>
            <Col>
                <Form.Control type="text" value={handleText} onChange={changeHandler} placeholder="Enter handle to pull" />
            </Col>
        </Row>
        <Row className='input-row'>
            <Col>
                <Button variant="primary" onClick={getPull}>Pull</Button>
                {' '}
                <Button variant="success" onClick={onAddClicked}>Add</Button>
            </Col>
        </Row>
        <Row>
            <Col>
                <ListGroup className="handles-lg">
                    {handles.map((x, i) => <ListGroup.Item key={i}>
                        {x}
                        <span onClick={groupItemClickedBuilder(i)} className="delete-btn-span">
                        <Button variant="danger" size="sm">
                        delete
                        </Button>
                        </span>
                    </ListGroup.Item>)}
                </ListGroup>
            </Col>
        </Row>
    </Container>
); 

In addition to the UI component, we will want to implement the three UI event handlers that handle data changes. The getPull event handler, which calls the API, will be implemented in the next section.

// set the handleText to current event value
const textChangeHandler = (e) => {
    e.preventDefault();
    setHandleText(e.target.value);
}

// Add handleText to handles, and then empty the handleText
const onAddClicked = (e) => {
    e.preventDefault();
    const newHandles = [...handles, handleText];
    setHandles(newHandles);
    setHandleText('');
}

// Remove the clicked handle from the list
const groupItemClickedBuilder = (idx) => (e) => {
    e.preventDefault();
    const newHandles = [...handles];
    newHandles.splice(idx, 1);
    setHandles(newHandles);
}

Now, we’re ready to implement the API call.

API Call

For the API call, we want to take the handles that we want to pull, send that over to the Python API in a POST request, and put the resulting JSON result into the curatedTweets variable. Then, if everything goes well, we want to programmatically navigate to the /display route. Otherwise, we will log the error to the console so we can debug more easily.

In code mode, it looks like this:

const pullAPI = (e) => {
    e.preventDefault();
    fetch('http://prismatic.pythonanywhere.com/get_tweets', {
        method: 'POST',
            mode: 'cors',
            headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            },
            body: JSON.stringify({
            accounts: handles
            })
        }).then(r=>r.json()).then(j => {
        setCuratedTweets(j);
            history.push('/display');
        })
    .catch(e => {
        console.log(e);
        })
}

And with that, we should be good to go. Feel free to run the app, add a couple of handles, and send a request over to the API.

Now, we’re ready to code the sentiment page.

Sentiment Sorted Mode

Because the Python API already sorts the tweets by sentiment, once we have the result from the Python API, the sentiment page is actually not too difficult.

Big-picture Plan

We will want a list interface to show the tweets. We will also want a couple of navigational components to switch to topic grouping mode and to go back to the Input page.

To start, let’s define the SentimentDisplay mode sub-component in the display.js file.

SentimentDisplay Component

The SentimentDisplay will take the curatedTweets object and display the sentiment-sorted tweets in a list. With the help of React-Bootstrap, the component is quite simple:

const SentimentDisplay = ({curatedTweets}) => {
    return <ListGroup>
      {curatedTweets.sentimentSorted.map((x, i) =>
            <ListGroup.Item key={i}>
                <div className="account-div">{x.account}:</div>
                <span className="tweet-span">{x.tweet}</span>
                <span className="sentiments-span">({x.data.sentiment.toPrecision(2)})</span>
            </ListGroup.Item>
        )}
    </ListGroup>
}

While we’re at it, let’s also add some styling. Put the following into display.css and import it:

 .account-div {
    font-size: 12px;
    font-weight: 600;

}

.tweet-span {
    font-size: 11px;
    font-weight: 500;
}

.sentiments-span {
    font-size: 10px;
}

.tl-container {
    margin-top: 10px;
}

.disp-row {
    margin-top: 5px;
}

We can now show the SentimentDisplay component. Change the Display function like so:

const Display = ({curatedTweets}) => {
    return <SentimentDisplay curatedTweets={curatedTweets} />
};

Let’s also take this opportunity to code up the navigational components. We will want two buttons - the “Back to edit” button and the Topic Group Mode.

We can implement these buttons in a separate Bootstrap row right above the SentimentDisplay component, like so:

Return <Container className="tl-container">
    <Row>
        <Col>
            <Link to="/"><Button variant='primary'>Back</Button></Link>
            {' '}
           <Button variant='success'>View by Topic</Button>
        </Col>
    </Row>
    <Row className="disp-row">
        <Col>
            <SentimentDisplay curatedTweets={curatedTweets} />
        </Col>
    </Row>
</Container>

Run the app and pull the tweets from a couple of handles. Looks pretty nifty!

Topic Grouping Mode

Now, we want to implement the Topic Grouping Mode. It’s a bit more complex than the SentimentDisplay, but again, some very handy Bootstrap components help us out immensely.

Big-picture Plan

We will get all the noun phrases and display them as an accordion list. We will then render the tweets containing the noun phrases once the accordion list is expanded.

Implementing Switching to Topic Grouping Mode

First, let’s implement logic to switch from Sentiment Mode to Topic Grouping Mode. Let’s start by creating the stub component first:

const TopicDisplay = () => {
    return <div>Topic Display</div>
}

And set some logic to create a mode to display it. In the main Display Component, add the following lines to create the logic for which component gets displayed.

// controls the display mode. Remember to import {useState} from ‘react’
const [displayType, setDisplayType] = useState('Sentiment');

// Switch the Display Mode
const toggleDisplayType = () => {
    setDisplayType(displayType === 'Sentiment' ? 'Topic': 'Sentiment');
}

// determines the text on the mode switch button
const switchStr = displayType === 'Sentiment'? 'View by Topic': 'View by Sentiment'

And change the JSX to the following to add the logic:

Return <Container className="tl-container">
    <Row>
        <Col>
            <Link to="/"><Button variant='primary'>Back</Button></Link>
            {' '}
            <Button variant='success' onClick={toggleDisplayType}>{switchStr}</Button>
        </Col>
    </Row>
    <Row className="disp-row">
        <Col>
                {
                    displayType === 'Sentiment'?
                    <SentimentDisplay curatedTweets={curatedTweets} />:
                    <TopicDisplay curatedTweets={curatedTweets} />
                }
        </Col>
    </Row>
</Container>

Now, you should see the Topic Group Display stub when you toggle.

The TopicDisplay Component

Now, we are ready to code the TopicDisplay component. As discussed before, it will leverage the Bootstrap Accordion List. The implementation is actually fairly simple:

const TopicDisplay = ({curatedTweets}) => {
    return <Accordion>
        {Object.keys(curatedTweets.groupedByNp).map((x, i) =>
            <Card key={i}>
                <Card.Header>
                    <Accordion.Toggle as={Button} variant="link" eventKey={i}>
                        {x} ({curatedTweets.groupedByNp[x].length})
                    </Accordion.Toggle>
                </Card.Header>
                <Accordion.Collapse eventKey={i}>
                    <Card.Body>
                        <ListGroup>
                            {curatedTweets.groupedByNp[x].map((y, i2) =>
                                <ListGroup.Item key={i2}>
                                    <div className="account-div">{y.account}:</div>
                                    <span className="tweet-span">{y.tweet}</span>
                                 <span className="sentiments-span">({y.data.sentiment.toPrecision(2)})</span>
                                </ListGroup.Item>
                            )}
                        </ListGroup>
                    </Card.Body>
                </Accordion.Collapse>
            </Card>
        )}
  </Accordion>
}

Run the app, and you should see the Topic Display.

Now the app is done, and we are ready to build the app for the emulator.

Running the App in the Emulator

Cordova makes it very easy to run the app in the emulator. Simply run:

cordova platform add ios # if you haven’t done so already
cordova run ios

And you should see the app in the emulator. Because Bootstrap is a responsive web app, the web app adapts to the width of an iPhone, and everything looks quite nice.

With the Cordova app done, let’s now look at the Ionic implementation.

Ionic-React Implementation

Ionic Primer

Ionic is a web component library and CLI toolkit that makes building hybrid applications easier. Originally, Ionic was built on top of AngularJS and Cordova, but they have since released their components in React.js and began supporting Capacitor, a platform similar to Cordova. What sets Ionic apart is that even though you are using web components, the components feel very similar to a native mobile interface. Additionally, the look and feel of the Ionic components automatically adapt to the operating system it runs on, which again helps the application look and feel more native and natural. Finally, while this is outside of the scope of our article, Ionic also provides several build tools that make deploying your application a bit easier.

For our application, we will be using Ionic’s React components to build the UI while leveraging some of the JavaScript logic we built during the Cordova section.

Configuring the App

First, we will want to install the Ionic tools. So let’s run the following:

npm install -g @Ionic/cli native-run cordova-res

And after the install is done, let’s go to the project folder. Now, we use the Ionic CLI to create our new project folder:

ionic start twitter-curation-Ionic blank --type=react --capacitor

Watch the magic happen, and now go into the folder with:

cd twitter-curation-Ionic

And run the blank app with:

ionic serve

Our app is thus set up and ready to go. Let’s define some routes.

Before we move on, you will notice that Ionic started the project using TypeScript. While I don’t go out of my way to use TypeScript, it has some very nice features, and we will be using it for this implementation.

Router Setup

For this implementation, we will use three routes - input, sentimentDisplay, and topicDisplay. We do this because we want to take advantage of the transition and navigation features provided by Ionic and because we’re using Ionic components, and accordion lists do not come prepackaged with Ionic. We can implement our own, of course, but for this tutorial, we will stay with the provided Ionic components.

If you navigate to the App.tsx, you should see the basic routes already defined.

Input Page

Big-picture Plan

We’ll be using a lot of the similar logic and code as the Bootstrap implementation, with a few key differences. First, we will use TypeScript, which means we will have type annotations for our code, which you will see in the next section. Second, we will be using Ionic components, which are very similar in style to Bootstrap but will be OS-sensitive in its styling. Lastly, we will be navigating dynamically using the history API like in the Bootstrap version but accessing the history slightly differently due to the Ionic Router implementation.

Setting Up

Let’s begin by setting up the input component with a stub component. Create a folder under pages named input and create under that a file named Input.tsx. Inside that file, put the following code to create a React component. Notice that because we’re using TypeScript, it’s a little bit different.

import React, {useState} from 'react';

const Input : React.FC = () => {
    return <div>Input</div>;
}

export default Input;

And change the component in App.tsx to:

const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
        <Route path="/input" component={Input} exact={true} />
        <Route exact path="/" render={() => <Redirect to="/input" />} />
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
);

Now, when you refresh the app, you should see the Input stub component.

Data Containers

Let’s create the data containers now. We want the containers for inputted Twitter handles as well as the current contents of the input box. Because we’re using TypeScript, we’ll need to add the type annotations to our useState invocation in the component function:

const Input : React.FC = () => {
    const [text, setText] = useState<string>('');
    const [accounts, setAccounts] = useState<Array<string>>([]);
    return <div>Input</div>;
}

We will also want a data container to hold the return values from the API. Because the content of that needs to be shared with the other routes, we define them at the App.tsx level. Import useState from React in the App.tsx file and change the app container function to the below:

const App: React.FC = () => {
  const [curatedTweets, setCuratedTweets] = useState<CuratedTweets>({} as CuratedTweets);
  return (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
        <Route path="/input" component={Input} exact={true} />
        <Route exact path="/" render={() => <Redirect to="/input" />} />
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
  );
}

At this point, if you are using an editor with syntax highlighting like Visual Studio Code, you should see the CuratedTweets light up. This is because the file does not know what the CuratedTweets interface looks like. Let’s define that now. Create a folder under src called interfaces and create a file inside it called CuratedTweets.tsx. In the file, define the CuratedTweets interface as follows:

 interface TweetRecordData {
    noun_phrases: Array<string>,
    sentiment: number
}

export interface TweetRecord {
    account: string,
    data: TweetRecordData,
    tweet: string
}

export default interface CuratedTweets {
    groupedByNp: Record<string, Array<TweetRecord>>,
    processedTweets: Array<TweetRecord>,
    sentimentSorted: Array<TweetRecord>
}

Now the app knows about the structure of the API return data. Import the CuratedTweets interface in App.tsx. You should see the App.tsx compile with no problem now.

We need to do a couple more things here. We need to pass the setCuratedTweets function into the Input component and make the Input component aware of this function.

In the App.tsx, modify the Input route like so:

<Route path="/input" render={() => <Input setCuratedTweets={setCuratedTweets}/>} exact={true} />

Now, you should see the editor flag down something else - Input does not know about the new prop being passed to it so we will want to define it in Input.tsx.

First, import the CuratedTweets interface, then define the ContainerProps interface like so:

interface ContainerProps {
    setCuratedTweets: React.Dispatch<React.SetStateAction<CuratedTweets>>
}

And finally, change the Input component definition like so:

const Input : React.FC<ContainerProps> = ({setCuratedTweets}) => {
    const [text, setText] = useState<string>('');
    const [accounts, setAccounts] = useState<Array<string>>([]);
    return <div>Input</div>;
}

We are done defining the data containers, and now, onto building the UI components.

UI Components

For the UI component, we will want to build an input component and a list display component. Ionic provides some simple containers for these.

Let’s start by importing the library components we’ll be using:

import { IonInput, IonItem, IonList, IonButton, IonGrid, IonRow, IonCol } from '@Ionic/react';

Now, we can replace the stub component with the IonInput, wrapped in an IonGrid:

return <IonGrid>
    <IonRow>
        <IonCol>
            <IonInput
              value={text}
              placeholder="Enter accounts to pull from"
              onIonChange={e => setText(e.detail.value!)} />
      </IonCol>
    </IonRow>
</IonGrid>

Notice that the event listener is onIonChange instead of onChange. Otherwise, it should look very familiar.

When you open the app in your browser, it may not look like the Bootstrap app. However, if you set your browser to emulator mode, the UI will make more sense. It will look even better once you deploy it on mobile, so look forward to it.

Now, let’s add some buttons. We will want an “Add to list” button and a “Pull API” button. For that, we can use IonButton. Change the size of the input’s IonCol to 8 and add the following two buttons with columns:

<IonCol size="8">
            <IonInput
              value={text}
              placeholder="Enter accounts to pull from"
              onIonChange={e => setText(e.detail.value!)} />
        </IonCol>
        <IonCol size="2">
            <IonButton style={{float: 'right'}} color="primary" size="small" onClick={onAddClicked}>Add</IonButton>
        </IonCol>
        <IonCol size="2">
           <IonButton style={{float: 'right'}} color="success" size="small" onClick={onPullClicked}>Pull</IonButton>
        </IonCol>

Since we’re writing the buttons, let’s write the event handlers as well.

The handler to add a Twitter handle to the list is simple:

const onAddClicked = () => {
        if (text === undefined || text.length === 0) {
            return;
        }
        const newAccounts: Array<string> = [...accounts, text];
        setAccounts(newAccounts);
        setText('');
    }

We will implement the API call in the next section, so let’s just put a stub function for onPullClicked:

const onPullClicked = () => {}

Now, we need to write the component for displaying the list of handles that has been inputted by the user. For that, we will use IonList, put into a new IonRow:

<IonRow>
    <IonCol>
        <IonList>
            {accounts.map((x:string, i:number) => <IonItem key={i}>
                <IonGrid>
                    <IonRow>
                        <IonCol size="8" style={{paddingTop: '12px'}}>{x}</IonCol>
                        <IonCol><IonButton style={{float: 'right'}} color="danger" size="small" onClick={deleteClickedBuilder(i)}>Delete</IonButton></IonCol>
                    </IonRow>
                </IonGrid>
            </IonItem>)}
        </IonList>
    </IonCol>
</IonRow>

Each list item is displaying the handle and a delete button in its very own IonGrid. For this code to compile, we will want to implement the deleteClickedHandler as well. It should be very familiar from the previous section but with TypeScript annotations.

const deleteClickedBuilder = (idx: number) => () => {
    const newAccounts: Array<string> = [...accounts];
    newAccounts.splice(idx, 1);
    setAccounts(newAccounts);
}

Save this, and you should see the Input page with all the UI components implemented. We can add handles, delete handles, and click the button to invoke the API.

As a final exercise, let’s move the in-line styles to CSS. Create a file in the input folder called input.css and import it in the Input.tsx file. Then, add the following styles:

.input-button {
    float: right;
}

.handle-display {
    padding-top: 12px;
}

Now, add className="input-button” on all of the IonButtons and className=”handle-display” on the handle list item IonCol that is displaying the intended Twitter handle. Save the file, and you should see everything looking quite good.

API Call

The code to pull the API is very familiar from the previous section, with one exception - we have to get access to the history component to be able to dynamically change routes. We will do this using the withHistory hook.

We first import the hook:

import { useHistory } from 'react-router';

And then implement the handler in the input component:

const history = useHistory();

const switchToDisplay = () => {
    history.push('/display');
}

const onPullClicked = () => {
    fetch('http://prismatic.pythonanywhere.com/get_tweets', {
        method: 'POST',
        mode: 'cors',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            accounts
        })
    }).then(r=>r.json()).then(j => {
        setCuratedTweets(j);
        switchToDisplay();
    })
    .catch(e => {
        console.log(e);
    })

}

Adding a Header

Our Input page looks quite nice, but it looks a little bare due to Ionic’s mobile-centric styling. To make the UI look more natural, Ionic provides a header feature that lets us provide a more natural user experience. When running on mobile, the header will also simulate the native OS’s mobile platform, which makes the user experience even more natural.

Change your component import to:

import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonInput, IonItem, IonList, IonButton, IonGrid, IonRow, IonCol } from '@Ionic/react';

And now wrap the UI in an Ionic page with a header, like so:

return <IonPage>
    <IonHeader>
      <IonToolbar>
        <IonTitle>Twitter Curation App</IonTitle>
      </IonToolbar>
    </IonHeader>
    <IonContent>
      <IonHeader collapse="condense">
        <IonToolbar>
        <IonTitle size="large">Twitter Curation App</IonTitle>
        </IonToolbar>
      </IonHeader>
       <IonGrid>
        <IonRow>
            <IonCol size="8">
                <IonInput
                value={text}
                placeholder="Enter accounts to pull from"
                onIonChange={e => setText(e.detail.value!)} />
            </IonCol>
            <IonCol size="2">
                <IonButton className="input-button" color="primary" size="small" onClick={onAddClicked}>Add</IonButton>
            </IonCol>

            <IonCol size="2">
            <IonButton className="input-button" color="success" size="small" onClick={onPullClicked}>Pull</IonButton>
            </IonCol>

        </IonRow>

        <IonRow>
            <IonCol>
                <IonList>
                    {accounts.map((x:string, i:number) => <IonItem key={i}>
                        <IonGrid>
                            <IonRow>
                                <IonCol size="8" className="handle-display">{x}</IonCol>
                                <IonCol><IonButton className="input-button" color="danger" size="small" onClick={deleteClickedBuilder(i)}>Delete</IonButton></IonCol>
                            </IonRow>
                        </IonGrid>
                 </IonItem>)}
                </IonList>
            </IonCol>
        </IonRow>
    </IonGrid>
    </IonContent>
  </IonPage>

Now that looks nice!

Sentiment Sorted Page

Big-picture Plan

The sentiment sorted page will be largely similar to the sentiment sorted page from the Bootstrap page but using TypeScript and Ionic. We will also implement the Topic Display as a separate route to take advantage of Ionic’s navigation features while running on mobile, so we will need to give this page the ability to navigate to the Topic Display route as well.

Route Setup

Let’s begin by creating a new folder called sentimentsorted and a file named SentimentSorted.tsx underneath. Export a stub component like so:

import React from 'react';

const SentimentSorted: React.FC = () => {
    return <div>Sentiment Sorted</div>
}
export default SentimentSorted;

And in the App.tsx, import the component:

import SentimentSorted from './pages/sentimentsorted/SentimentSorted';

And add the route:

<Route path="/display" render={() => <SentimentSorted curatedTweets={curatedTweets} />} />

You will get a TypeScript error saying that SentimentSorted is not expecting the curatedTweets props, so let’s take care of that now in the next section.

UI Components

Let’s begin by defining the container’s props. Much like the input component:

import CuratedTweets from '../../interfaces/CuratedTweets';

interface ContainerProps {
    curatedTweets: CuratedTweets
}

And now, change the stub display:

const SentimentSorted: React.FC<ContainerProps> = ({curatedTweets}) => {
    return <div>Sentiment Sorted</div>
}

And everything should compile.

The display is very simple, it’s just an IonList with display components:

return <IonGrid>
    <IonRow>
        <IonCol>
        <IonList>
            {(curatedTweets.sentimentSorted).map((x, i) =>
            <IonItem key={i}>
                <IonLabel className="ion-text-wrap">
                    <h2>{x.account}:</h2>
                    <h3>{x.tweet}</h3>
                    <p>({x.data.sentiment.toPrecision(2)})</p>
                </IonLabel>
            </IonItem>)}
        </IonList>
        </IonCol>
    </IonRow>
</IonGrid>

If you save and pull some tweets using the input component, you should see the tweets displayed in a list.

Now, let’s add the navigation buttons. Add to the IonGrid:

<IonRow>
    <IonCol>
        <IonButton color='primary' onClick={switchToInput}>Back</IonButton>
        <IonButton color="success" onClick={toggleDisplayType}>{switchStr}</IonButton>
    </IonCol>
</IonRow>

The switchToInput is very easy to implement with the history API:

const switchToInput = () => {
    history.goBack();
}

And ToggleDisplayType should be familiar as well:

const toggleDisplayType = () => {
    setDisplayType(displayType === 'Sentiment' ? 'Topic': 'Sentiment');
}

const switchStr = displayType === 'Sentiment'? 'View by Topic': 'View by Sentiment'

Now we have the SentimentDisplay component implemented. Now, before we implement the Topic Display Page, we need to implement the component that displays all the topics. We will do that in the next section.

Topic Groups Component

Let’s add a topic list display option and display it conditionally. To do that, we need to break out the Sentiment Display list. Rename SentimentDisplay to Display, and let’s break out the sentiment display list:

interface SentimentDisplayProps {
    sentimentSorted: Array<TweetRecord>
}

const SentimentDisplay: React.FC<SentimentDisplayProps> = ({sentimentSorted}) => {
    return <IonList>
        {(sentimentSorted || []).map((x, i) =>
        <IonItem key={i}>
            <IonLabel className="ion-text-wrap">
                <h2>{x.account}:</h2>
                <h3>{x.tweet}</h3>
                <p>({x.data.sentiment.toPrecision(2)})</p>
            </IonLabel>
        </IonItem>)}
    </IonList>
}

Notice how we are using one of the class definitions in the CuratedTweets interface. That’s because these components do not need the entire CuratedTweets object but only a subset. The topic list is very similar:

interface TopicDisplayProps {
    groupedByNP: Record<string, Array<TweetRecord>>
}

const TopicDisplay: React.FC<TopicDisplayProps> = ({groupedByNP}) => {
    return <IonList>
        {Object.keys(groupedByNP || {}).map((x, i) =>
        <IonItem key={i}  routerLink={`/topicDisplay/${encodeURIComponent(x)}`}>
            <IonLabel className="ion-text-wrap">
                <h2>{x} ({groupedByNP[x].length})</h2>
            </IonLabel>
        </IonItem>)}
    </IonList>
}

And now, conditional display is easy to set up in the Display Component:

return (
    <IonGrid>
        <IonRow>
            <IonCol>
                <IonButton color='primary' onClick={switchToInput}>Back</IonButton>
                <IonButton color="success" onClick={toggleDisplayType}>{switchStr}</IonButton>
            </IonCol>
        </IonRow>
        {
            displayType === 'Sentiment'? <SentimentDisplay sentimentSorted={curatedTweets.sentimentSorted} /> :
            <TopicDisplay groupedByNP={curatedTweets.groupedByNp} />
         }
    </IonGrid>
);

Make sure to change the default export, and now we’re ready to implement the Topic Display Page.

Topic Display Page

Big-picture Plan

The topic display page is a list display similar to the sentiment display, but we will be looking for the topic in question from the route parameter.

Route Setup

If you’ve gotten this far, you should already know what to do. Let’s create a page folder called topicdisplay and a TopicDisplay.tsx, write a stub component, and import it into the App.tsx page. Now, let’s set up the routes:

<Route path="/topicDisplay/:topic" render={() => <TopicDisplay curatedTweets={curatedTweets} /> } />

Now we’re ready to implement the UI component.

UI Components

First, let’s create the ContainerProps definition:

interface ContainerProps {
    curatedTweets: CuratedTweets
}

const TopicDisplay: React.FC<ContainerProps> = ({curatedTweets}) => {
    Return <div>Topic Display</div>
}

Now, we will need to retrieve the topic from the URL path name. To do that, we will be using the history API. So let’s import useHistory, instantiate the history API, and pull the topic from the pathname. While we’re at it, let’s also implement the switch back functionality:

const TopicDisplay: React.FC<ContainerProps> = ({curatedTweets}) => {
    const history = useHistory();
    const switchToDisplay = () => {
        history.goBack();
    }
    const topic = history.location.pathname.split('/')[2];
    const tweets = (curatedTweets.groupedByNp || {})[topic];

Now that we have the tweets with that particular topic, displaying is actually quite simple:

return (
    <IonGrid>
        <IonRow>
            <IonCol>
                <IonButton color='primary' onClick={switchToDisplay}>Back</IonButton>
            </IonCol>
        </IonRow>
        <IonRow>
            <IonCol>
                <IonList>
                    {(tweets || []).map((x, i) => <IonItem key={i}>
                        <IonLabel className="ion-text-wrap">
                            <h2>{x.account}:</h2>
                            <h3>{x.tweet}</h3>
                            <p>({x.data.sentiment.toPrecision(2)})</p>
                        </IonLabel>
                    </IonItem>)}
                </IonList>
            </IonCol>
        </IonRow>
    </IonGrid>
);

Save and run, and things should be looking good.

Running the App in the Emulator

To run the app in the emulator, we simply run a few Ionic commands to add the mobile platform and copy the code over, similar to how we set things up with Cordova.

ionic build # builds the app
ionic cap add ios # adds iOS as one of the platforms, only have to run once
ionic cap copy # copy the build over
ionic cap sync # only need to run this if you added native plugins
ionic cap open ios # run the iOS emulator

And you should see the app show up.

React Native Implementation

React Native Primer

React Native takes a very different approach from the web-based approaches of the previous sections. React Native renders your React code as native components. This comes with several advantages. First, the integration with the underlying operating system is much deeper, which allows the developer to take advantage of new smartphone features and OS-specific features that may not be available through Cordova/Capacitor. Second, because there is no web-based rendering engine in the middle, a React Native app is generally faster than one written using Cordova. Finally, because React Native allows integration of native components, developers can exert much finer-grained control over their application.

For our application, we will use the logic from the previous sections and use a React Native component library called NativeBase to code up our UI.

Configuring the App

First, you will want to install all the requisite components of React Native following the instructions here.

Once React Native is installed, let’s start the project:

react-native init TwitterCurationRN

Let the setup script run, and eventually, the folder should be created. Cd into the folder and run react-native run-ios, and you should see the emulator pop up with the example app.

We will want to also install NativeBase since that is our component library. To do that, we run:

npm install --save native-base
react-native link

We also want to install the React Native stack navigator. Let’s run:

npm install --save @react-navigation/stack @react-navigation/native

And

react-native link
cd ios
pod-install
cd

To complete the linking and installation of the native plugins.

Router Setup

For routing, we will use the stack navigator that we installed in the above step.

Import the router components:

import { NavigationContainer } from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';

And now, we create a stack navigator:

const Stack = createStackNavigator();

Change the content of the App component to use the stack navigator:

const App = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
        <NavigationContainer>
          <Stack.Navigator initialRouteName="Entry">
            <Stack.Screen name="Entry" component={Entry} />
          </Stack.Navigator>
        </NavigationContainer>
    </>
  );
};

At this point, you will get an error because Entry is not yet defined. Let’s define a stub element just to make it happy.

Create a components folder in your project, create a file named Entry.jsx, and add a stub component like so:

import React, {useState} from 'react';
import { Text } from 'native-base';

export default Entry = ({navigation}) => {
    return <Text>Entry</Text>; // All text must be wrapped in a <Text /> component or <TextView /> if you’re not using NativeBase.
}

Import the Entry component in your app, and it should build.

Now, we’re ready to code the Input page.

Input Page

Big-picture Plan

We will be implementing a page that is very similar to the one implemented above but using NativeBase components. Most of the JavaScript and React APIs we used, such as hooks and fetch, are all still available.

The one difference will be the way we work with the navigation API, which you will see later.

UI Components

The additional NativeBase components we will use are Container, Content, Input, List, ListItem, and Button. These all have analogs in Ionic and Bootstrap React, and the builders of NativeBase have made it very intuitive for people familiar with these libraries. Simply import like so:

import { Container, Content, Input,

    Item, Button, List, ListItem, Text } from 'native-base';

And the component is:

return <Container>
        <Content>
          <Item regular>
            <Input placeholder='Input Handles Here' onChangeText={inputChange}
          value={input} />
            <Button primary onPress={onAddClicked}><Text> Add </Text></Button>
            <Text> </Text>
            <Button success onPress={onPullClicked}><Text> Pull </Text></Button>
          </Item>
        <Item>
                <List style={{width: '100%'}}>
                   {handles.map((item) => <ListItem key={item.key}>
                        <Text>{item.key}</Text>
                    </ListItem>)}
                </List>
          </Item>
        </Content>
      </Container>

And now, let’s implement the state and event handlers:

const [input, changeInput] = useState('');
    const [handles, changeHandles] = useState([]);
    const inputChange = (text) => {
        changeInput(text);
    }

   const onAddClicked = () => {
        const newHandles = [...handles, {key: input}];
        changeHandles(newHandles);
        changeInput('');
    }

And finally, the API call:

const onPullClicked = () => {
    fetch('http://prismatic.pythonanywhere.com/get_tweets', {
        method: 'POST',
        mode: 'cors',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            accounts: handles.map(x => x.key)
        })
    }).then(r=>r.json()).then(data => {
        navigation.navigate('SentimentDisplay', { data });
    })
    .catch(e => {
        console.log(e);
    })
}

Notice that this implementation is the same as in the NativeBase implementation, except we are navigating in a different way. The stack navigator passes into its components a prop called “navigation,” and that can be used to navigate between routes with .navigate. In addition to simply navigating, you can also pass data to the target component. We will use this mechanism to pass the data.

To make the app compile, we still need to make Entry aware of the navigation component. To do that, we will need to change the component function declaration:

export default Entry = ({navigation}) => {

Now save, and you should see the page.

Sentiment Sorted Page

Big-picture Plan

We will be implementing the sentiment page much like the previous sections, but we will be styling the page a little differently, and we will also be using the navigation library differently to get the API call return value.

Because React Native doesn’t have CSS, we will either need to define a StyleSheet object or simply code the styles in-line. Because we will share some of the styling across components, let’s create a global stylesheet. We will do that after the route setup.

Also, because StackNavigator has an in-built Back navigation button, we won’t need to implement our own Back button.

Route Setup

Route definition in StackNavigator is very simple. We simply create a new one named Stack Screen and give it the component, much like the React router.

 <NavigationContainer>
    <Stack.Navigator initialRouteName="Entry">
        <Stack.Screen name="Entry" component={Entry} />
        <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} />
    </Stack.Navigator>
</NavigationContainer>

To make this work, we will of course need to create a stub component in components/SentimentDisplay.js:

import React from 'react';
import {Text} from 'native-base';

const SentimentDisplay = () => {
    return <Text>Sentiment Display</Text>;
}

export default SentimentDisplay;

And import it:

import SentimentDisplay from './components/SentimentDisplay';

Now, we’re ready to create the global stylesheet.

Global StyleSheet

First, create a file named globalStyles.js. Then, import the StyleSheet component from React Native and define the styles:

import {StyleSheet} from 'react-native';

export default StyleSheet.create({
    tweet: {paddingTop: 5},
    accountName: {fontWeight: '600'},
})

And we’re ready to code the UI.

UI Components

The UI component is pretty familiar, with the exception of how we work with the routes. We will want to use StackNavigator’s special props navigation and route to get the current application state and to navigate to the topic display should the user want to see that page.

Change the component definition to access the navigation props:

const SentimentDisplay = ({route, navigation}) => {

And now, we implement the application state read and the navigation functionality:

const {params: {data}} = route;
const viewByTopicClicked = () => {
    navigation.navigate('TopicDisplay', { data });
}

Import the global styles:

import globalStyles from './globalStyles';

And the components:

import { View } from 'react-native';
import {List, Item, Content, ListItem, Container, Text, Button} from 'native-base'; 

And finally, the components:

return <Container>
    <Content>
        <Item>
         <Button primary onPress={viewByTopicClicked}><Text>View By Topic</Text></Button>
        </Item>
        <Item>
            <List style={{width: '100%'}}>
                {data.sentimentSorted.map((item, i) => <ListItem key={i}>
                    <View style={globalStyles.listItem}>
                    <Text style={globalStyles.accountName}>{item.account}:</Text>
                    <Text style={globalStyles.tweet}>{item.tweet} ({item.data.sentiment.toPrecision(2)})</Text>
                    </View>
                </ListItem>)}
            </List>
        </Item>
    </Content>
</Container>;

Save and try to pull some tweets, and you should see the sentiment display. Now onto the topic grouping page.

Topic Grouping Page

Big-picture Plan

Topic Display is again very similar. We will be using a handler builder to build a navigation function to navigate to the display page for a specific topic item, and we will also be defining a stylesheet that is specific to this page.

One new thing we will be doing is implementing a TouchableOpacity, which is a React Native specific component that functions much like a button.

Route Setup

Route definition is the same as before:

<Stack.Navigator initialRouteName="Entry">
    <Stack.Screen name="Entry" component={Entry} />
    <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} />
    <Stack.Screen name="TopicDisplay" component={TopicDisplay} />
</Stack.Navigator>

The stub component components/TopicDisplay.js:

import React from 'react';
import {Text} from 'native-base';

const TopicDisplay = () => {
    return <Text>Topic Display</Text>;
} 

export default TopicDisplay;

And import it:

import TopicDisplay from './components/TopicDisplay;

UI Components

A lot of this will look very familiar. Import the library functions:

import {
   View,
   TouchableOpacity,
   StyleSheet
 } from 'react-native';
import {List, Item, Content, ListItem, Container, Text} from 'native-base';

Import the global styles:

import globalStyles from './globalStyles';

Define the custom styles:

const styles = StyleSheet.create({
    topicName: {fontWeight: '600'},
})

Define the navigation props:

export default TopicDisplay = ({route, navigation}) => {

Define the data and action handlers. Notice we’re using a handler builder, a function that returns a function:

const {params: {data}} = route;
const specificItemPressedHandlerBuilder = (topic) => () => {
    navigation.navigate('TopicDisplayItem', { data, topic });
}

And now, the components. Notice that we’re using a TouchableOpacity, which can have an onPress handler. We could have used TouchableTransparency as well, but TouchableOpacity’s click-and-hold animation was better suited for our application.

return <Container>
    <Content>
        <Item>
            <List style={{width: '100%'}}>
                {Object.keys(data.groupedByNp).map((topic, i) => <ListItem key={i}>
                    <View style={globalStyles.listItem}>
                    <TouchableOpacity onPress={specificItemPressedHandlerBuilder(topic)}>
                        <Text style={styles.topicName}>{topic}</Text>
                    </TouchableOpacity>
                    </View>
                </ListItem>)}
            </List>
        </Item>
    </Content>
 </Container>;

And this should do it. Save and try out the application!

Now, onto the Topic Display Item Page.

Topic Display Item Page

Big-picture Plan

The Topic Display Item page is very similar and all the idiosyncrasies are taken care of in the other sections so it should be smooth sailing from here.

Route Setup

We’ll add the route definition:

<Stack.Screen name="TopicDisplayItem" component={TopicDisplayItem} />

Add the import:

import TopicDisplayItem from './components/TopicDisplayItem';

And create the stub component. Instead of just a bare component, let’s also import the NativeBase components we’ll be using and define the route props:

import React from 'react';
import {View} from 'react-native';
import {List, Item, Content, ListItem, Container, Text} from 'native-base';
 
import globalStyles from './globalStyles';
 
const TopicDisplayItem = ({route}) => {
    const {params: {data, topic}} = route;
    return <Text>Topic Display Item</Text>;
}
 
export default TopicDisplayItem;

UI Components

The UI Component is quite simple. We have seen it before and we’re not really implementing any custom logic. So, let’s just go for it! Take a deep breath…

return <Container>
    <Content>
        <Item>
            <List style={{width: '100%'}}>
                {data.groupedByNp[topic].map((item, i) => <ListItem key={i}>
                    <View style={globalStyles.listItem}>
                    <Text style={globalStyles.accountName}>{item.account}:</Text>
                    <Text style={globalStyles.tweet}>{item.tweet} ({item.data.sentiment.toPrecision(2)})</Text>
                    </View>
                </ListItem>)}
            </List>
        </Item>
    </Content>
</Container>;

Save, and we should be good to go! Now we’re ready to run the app in the emulator… wait, haven’t we already?

Running the App

Well, since you’re working with React Native, you’re already running the app in the emulator so this part is already taken care of. This is one of the great things about React Native’s development environment.

Whew! With that, we’re done with the coding portion of this article. Let’s see what we learned about the technologies.

Comparing the Technologies

Cordova: Pros and Cons

The best thing about Cordova is the sheer speed with which a skilled web developer can code up something functional and reasonably presentable. Web development skills and experience transfer over easily because, after all, you are coding a web app. The development process is quick and simple, and accessing Cordova API is simple and intuitive as well.

The drawbacks of using Cordova directly comes mostly from the over-reliance on web components. Users have come to expect a specific user experience and interface design when using mobile apps, and when an application feels like a mobile site, the experience can be a bit jarring. Additionally, most of the features built into apps, such as transitional animations and navigation utilities, must be implemented manually.

Ionic: Pros and Cons

The best part of Ionic was how many mobile-centric features I got for “free.” By coding much like I would have coded a web application, I was able to build an app that looks a lot more mobile-friendly than simply using Cordova and React-Bootstrap. There was navigation animation, buttons with native-looking styles, and many user interface options that made the user experience very smooth.

The drawback of using Ionic was partially caused by its strengths. First, it was sometimes hard to envision how the app would behave in various environments. Just because the app looked one way did not mean that the same UI placement would look the same in another environment. Second, Ionic sits on top of many pieces of underlying technologies, and getting access to some of the components proved difficult. Lastly, this is specific to Ionic-React, but because Ionic was first built for Angular, many Ionic-React features seemed to have less documentation and support. However, the Ionic team seems very attentive to the needs of React developers and delivers new features quickly.

React Native: Pros and Cons

React Native had a very smooth user experience developing on mobile. By connecting directly to the emulator, it was no mystery how the application would look. The web-based debugger interface was extremely helpful in cross-applying debugging techniques from the web application world, and the ecosystem is quite robust.

The drawback of React Native comes from its proximity to the native interface. Many DOM-based libraries could not be used, which meant having to learn new libraries and best practices. Without the benefit of CSS, styling the application was somewhat less intuitive. Finally, with many new components to learn (e.g., View instead of div, Text component wrapping everything, Buttons vs. TouchableOpacity vs. TouchableTransparency, etc.), there is a bit of a learning curve in the beginning if someone is coming into the React Native world with little prior knowledge of the mechanics.

When to Use Each Technology

Because Cordova, Ionic, and React Native all have very strong pros and cons, each technology has a context in which it would enjoy the best productivity and performance.

If you already have an existing application that is web-first with a strong brand identity surrounding the UI design and general look and feel, your best option would be Cordova, which gives you access to the smartphone’s native features while letting you reuse most of your web components and preserve your brand identity in the process. For relatively simple applications using a responsive framework, you may be able to build a mobile app with very few changes required. However, your application will look less like an app and more like a web page, and some of the components people expect from a mobile app will be coded separately. Therefore, I recommend Cordova in cases where you’re in a web-first project porting the application over to mobile.

If you are starting to code a new application with an app-first philosophy, but your team’s skill set is primarily in web development, I recommend Ionic. Ionic’s library lets you quickly write code that looks and feels close to native components while letting you apply your skills and instincts as a web developer. I found that web development best practices readily cross-applied to developing with Ionic, and styling with CSS worked seamlessly. In addition, the mobile version of the site looks much more native than a website coded using a responsive CSS framework. However, at some steps along the way, I found that the React-Ionic-Native API integration requires some manual adjustment, which could prove time-consuming. Therefore, I recommend Ionic in cases where your application is being developed from the ground up and you want to share a significant amount of code between a mobile-capable web application and a mobile application.

If you are coding a new application with some native codebase implemented, you may want to try React Native. Even if you’re not using native code, it could also be the best option in cases where you’re already familiar with React Native, or when your primary concern is the mobile application rather than a hybrid application. Having focused most of my front-end development efforts on web development, I initially found that getting started with React Native had more of a learning curve than Ionic or Cordova due to the differences in component organization and coding conventions. However, once these nuances are learned, the coding experience is quite smooth, especially with the help of a component library like NativeBase. Given the quality of the development environment and control over the application, if your project’s front end is primarily a mobile application, I would recommend React Native as your tool of choice.

Future Topics

One of the topics that I did not have time to explore was the ease of accessing native APIs such as camera, geolocation, or biometric authentication. One of the great benefits of mobile development is the accessibility of a rich API ecosystem that is generally not accessible on the browser.

In future articles, I would like to explore the ease of developing these native API-enabled applications using various cross-platform technologies.

Conclusion

Today, we implemented a Twitter curation app using three different cross-platform mobile development technologies. I hope this gave you a good sense of what each technology is like and inspired you to develop your own React-based application.

You can find the code associated with the tutorial on GitHub.

Understanding the basics

  • How difficult is natural language processing (NLP)?

    NLP is a machine learning umbrella of techniques that encompass understanding and responding to or reporting on meaning, context, mood, and other attributes. Many of these techniques are complex and require significant processing power. Fortunately, there are open-source libraries that do much of the heavy lifting, making NLP accessible to many engineers and applications.

  • How does natural language processing work?

    There are a number of different techniques, depending on the goal. In general, these techniques parse sentences, then look for contextual cues in surrounding words or sentence structure to derive deeper meaning.

  • Is learning React worth it in 2020?

    This article shows without a doubt the value of React and its supporting frameworks. Each framework - Cordova, Ionic, and React Native - has value for sophisticated mobile apps. Learning React is a natural starting point.

Hire a Toptal expert on this topic.
Hire Now
Shanglun Wang

Shanglun Wang

Verified Expert in Engineering

New York, NY, United States

Member since December 16, 2016

About the author

Sean is a passionate polyglot: a full-stack wizard, sysadmin, and data scientist. He’s also developed market intelligence software.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

CB Insights

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.