Developing a Real World App in Ionic React
Tags: react, ionic, javascript, mobile
Published: 2019-06-07 16:49:31

---
title: Developing a Real World App in Ionic React
published: true
description: In this tutorial, we unleash the framework-agnostic capabilities of Ionic 4 to develop Conduit - a Medium clone Real-World App - using React.
tags: React, Ionic, JavaScript, Mobile
canonical_url: https://blog.jscrambler.com/developing-a-real-world-app-in-ionic-react/
cover_image: https://blog.jscrambler.com/content/images/2019/05/jscrambler-blog-developing-a.real-world-app-ionic-react.jpg
---

Ionic recently added support for the React and Vue frameworks. Traditionally, Ionic used Angular and Cordova frameworks to create their apps. With [Ionic 4](https://blog.jscrambler.com/introduction-to-ionic-4/), we can develop framework-agnostic apps. In this tutorial, we will create a basic Ionic app in the React Framework. 

### RealWorld and Conduit

The folks at Thinkster are behind a project known as RealWorld project. Conduit is a Medium clone which can be built using multiple stacks. We can mix and match different types of backends and frontends. Every backend and frontend follow the same [API spec](https://github.com/gothinkster/realworld/tree/master/spec). 

Unlike simple CRUD ToDo Apps, Conduit is a full-fledged CMS. Implementing the project requires some effort.

## Getting Started
For this tutorial, we will be covering the following points. 

- Home Page (With and Without Auth)
  - List of Articles 
  - Favorite an Article (Auth Required)
  - Navigate to Article Page 
  - Navigate to Author Profile Page
  - Display your Feed (Auth Required)
  - Display Popular Tags. 
  - Filter Articles by Tags.
- Article Page (With and Without Auth)
  - Read Article 
  - List Comments 
  - Add Comment (Auth Required)
  - Navigate to Author Page
- Settings Page (Auth required)
  - Modify user Settings
- Profile Page (With and Without Auth)
  - Display User details
  - Display Posts by user 
  - Display Posts Favorited by user 
  - Follow User (Auth Required)
- Create Article (Auth Required)
  - Create Article
- Login / Signup Page
  - Signup User
  - Login User

### Understanding the Conduit API

Conduit has a production API endpoint [here](https://conduit.productionready.io/api/). We will use that endpoint for this tutorial. Alternatively, you can set up your own backends using a variety of languages.

We will be using:

 * /articles:  For fetching articles, comments
 * /users: For creating and Authenticating users
 * /profiles: For following profiles
 * /tags: For fetching tags

Users are authenticated via a JWT token in headers. 


### Setting up the Project

The Ionic CLI currently doesn't support React templates. We will be using create-react-app to set up our project. Use:

```
npm i -g create-react-app
create-react-app conduit --typescript
```
Alternatively, we can use npx:

```
npx create-react-app conduit --typescript
```
Navigate to the folder and install Ionic and React router:

```
cd conduit
npm install @ionic/react react-router react-router-dom @types/react-router @types/react-router-dom
```

Let's initialize Ionic in this project.

```
ionic init "Conduit" --type=custom
```

We need to enable Capacitor in our project
```
ionic integrations enable capacitor
```
A file `capacitor.config.json` is generated in our project directory. In the file, we need to change the build directory from `"webDir": "www"` to `"webDir": "build"`.

Create a build folder OR generate a build using:

```
npm run build
```

Now, we can add a platform to our project.
```
npx capacitor add android

```
Additionally, we will be using this folder structure.

- src
  - components/
  - pages/
  - constants.ts

Let's set up our constants file.

```typescript
export interface AppConfig {    
    API_ENDPOINT : string
   
  }  
  
export const CONFIG: AppConfig = {
    API_ENDPOINT : "https://conduit.productionready.io/api/"
  };
```
In the `App.tsx` file, we need to import the Ionic CSS files.

```typescript
import '@ionic/core/css/core.css';
import '@ionic/core/css/ionic.bundle.css';
```

##  Developing the App

An offline user has access to the Home, Article, Profile and Login pages. An online user has access to the Home, Article, Profile, Settings and Login Pages.

There are 70+ Ionic components we can use to speed up our development. First, let's set up routing.

### Routing 

We are using React Router to manage Routing. We will be using `IonRouterOutlet` to manage routes. The key difference with the website is that users can't navigate with urls. We are also creating a custom component `SideMenu` for side menu navigation. `IonMenuToggle` is used to toggle the menu in components.

A loggedIn key and an Event Listener are used to manage user state. The token obtained from authentication will be saved in localstorage.

```typescript
class App extends Component {
  constructor(props: any){
    super(props);
  }
  render() {
    return (
      <Router>
        <div className="App">        
          <IonApp>
          <IonSplitPane contentId="main">
            <SideMenu></SideMenu>
            <IonPage id="main">        
                <IonRouterOutlet>
                    <Route exact path="/" component={HomePage} />
                    <Route exact path="/login" component={LoginPage} />  
                    <Route path="/article/:slug" component={ArticlePage} />
                    <Route path="/profile/:authorname" component={ProfilePage} />
                    <Route path="/settings" component={SettingsPage} />
                    <Route path="/newarticle" component={NewArticlePage} />                             
                </IonRouterOutlet>
            </IonPage>
            </IonSplitPane>
          </IonApp>
        </div>
      </Router>
    );
  }
}

export default App;
```

<a href="https://blog.jscrambler.com/protecting-hybrid-mobile-apps-with-ionic-and-jscrambler/?utm_source=dev.to&utm_medium=referral" target="_blank"><img alt="Protect your Code with Jscrambler" src="https://media.jscrambler.com/blog/V1-Ionic.jpg"></a>

Side menu Component:

```typescript
class SideMenu extends React.Component<any, any> {
  constructor(props: any){
    super(props);
        this.state = {      
      isLoggedIn: localStorage.getItem("isLogin") ? localStorage.getItem("isLogin") :"false",   
      routes:  {
        appPages: [
          { title: 'Home', path: '/', icon: 'home' },         
        ],        
        loggedOutPages: [
          { title: 'Login', path: '/login', icon: 'log-in' },
        ]  }
    }
    window.addEventListener('loggedIn', (e: any) => {            
      this.setState({
        isLoggedIn : e['detail'].toString(),
        routes : {
          appPages: [
            { title: 'Home', path: '/', icon: 'home' },         
          ],
          loggedInPages: [
            { title: 'My Profile', path: '/profile/'+localStorage.getItem("username"), icon: 'person'},        
            { title: 'New Article', path: '/newarticle', icon: 'create' },
            { title: 'Settings', path: '/settings', icon: 'settings' },
            { title: 'Logout', path: '/login', icon: 'log-out' }
          ],
          loggedOutPages: [
            { title: 'Login', path: '/login', icon: 'log-in' },
          ]  }        
      })      
 
    });  
  }    
    
   renderMenuItem(menu: any) {
    return (
         <IonMenuToggle key={menu.title} auto-hide="false">
                   <IonItem>
                     <IonIcon name={menu.icon} ></IonIcon>
                     <Link replace className="sidemenu-link" to={menu.path} >{menu.title}</Link> 
                   </IonItem>
                   </IonMenuToggle>
      )
  }
  
  render() {
      return (  
        <IonMenu side="start" menuId="first" contentId="main">
          <IonHeader>
            <IonToolbar color="success">
              <IonTitle>Start Menu</IonTitle>
            </IonToolbar>
          </IonHeader>
          <IonContent>
            <IonList>
              {this.state.routes.appPages.map((art: any) => this.renderMenuItem(art))}
              {this.state.isLoggedIn === "true" ? <> {this.state.routes.loggedInPages.map((art: any) =>
                this.renderMenuItem(art))} </> :<> {this.state.routes.loggedOutPages.map((art: any) =>
                this.renderMenuItem(art))} </> }
            </IonList>
          </IonContent>
        </IonMenu>
      )
  }
}
export default SideMenu

```


### Home Page 

As mentioned above, the Home Page has a list of articles. The `/articles` endpoint provides us an array of 20 articles. 

A normal user can:

- View a list of articles
- Navigate to a full article
- Navigate to author profile
- Filter articles by tags

An authenticated user can additionally:

- View his/her article feed
- Favorite an article

We will create the `ArticleCard` and `TagsCloud` components to make our tasks simpler. 

Additionally, `Header` is a wrapper around Ionic Header with `title` as props.

```html
   <IonHeader>
          <IonToolbar >
          <IonTitle  class="header"  color="success">{this.props.title}</IonTitle>           
            <IonMenuButton slot="start"></IonMenuButton>
          </IonToolbar>
        </IonHeader> 
```

`TagsCloud` is a component to retrieve popular tags. We receive an array of tags at the endpoint `/tags`. We will be passing a function `onTagClick` in props. So, we will be able to filter the articles using the clicked tag. We are using `IonChip` to render the tag chips.

```typescript
handleClick(tag: any) {
    console.log(tag);
    this.props.onTagClick(tag);
  }
  
  componentDidMount() {    
    fetch(CONFIG.API_ENDPOINT+"tags")
      .then(res => res.json())
      .then(
        (res) => {
          this.setState({          
            tags: res.tags
          });
        },      
        (err) => {
          console.log(err)
        }
      )
  }
 
  render() {
    return (      
      <div className="tagcloud">
      <IonLabel text-center>
            Popular Tags
          </IonLabel>
          <div>        
         {this.state.tags.map((tag: any, index: number) => 
          <IonChip color="success" key={index}>
          <IonLabel  onClick={() => this.handleClick(tag)}>{tag}</IonLabel>
        </IonChip>           
             )}         
         </div>
      </div> 
    );       
  }
}  
```

The `ArticleCard` component returns an `IonItem` and `IonItemSliding` for logged in users. A user can swipe and favorite an article if he/she chooses to.

Let's create a function `loggedOutCard` to return an `IonItem`.

```typescript
loggedOutCard() {
  return (                 
           <IonItem >
          <IonAvatar slot="start">
              <img src={this.props.src} />              
            </IonAvatar>
            <IonLabel>
              <p className="title">{this.props.title}</p>              
              <IonGrid >
                <IonRow>
                  <IonCol class="author" size="6">
                  <Link className="link" to={this.profileLink}>
                  {this.props.author}</Link>                     
                 </IonCol>
                  <IonCol  size="6" text-right>                  
                  <Link className="link" to={this.routeLink}>Read More</Link>        
                  </IonCol>
                </IonRow>
              </IonGrid>
            </IonLabel>
          </IonItem>      
  )
}
```
We create a function `loggedInCard` to return an `IonSlidingItem`. 

```typescript
loggedInCard(){
  return (        
           <IonItemSliding>
          {this.loggedOutCard()}
        <IonItemOptions side="end">
          <IonItemOption  color={this.state.favorited ? "success": "light"} onClick={this.favoriteArticle}>
          <IonIcon color={this.state.favorited ? "light": "success"} class="icon-blog-card" name="heart" />{this.state.favoritesCount}</IonItemOption>
        </IonItemOptions>
      </IonItemSliding>           
  )
}
```
Authenticated users can favorite an article. We will create a function `favoriteArticle` to achieve this.

```typescript
favoriteArticle = (params: any) => {
  console.log(params);
  let url = CONFIG.API_ENDPOINT+"articles/" + this.props.slug + '/favorite';
  let method;
  if (!this.state.favorited) {
    method = 'POST'
  } else {
    method = "DELETE"
  }
  fetch(url, {
      method: method,
      headers: {
        "Content-Type": "application/json",
        "Authorization": "Token " + localStorage.getItem("token"),
      }
    })
    .then(res => res.json())
    .then(
      (res) => {       
        this.setState({
          favorited: res.article.favorited,
          favoritesCount: res.article.favoritesCount,
        })
      },
      (err) => {
        console.error(err);
      }
    )
}
```
Finally, let’s return the component based on user state.

```typescript
  render() {   
      return (
        <>
        {localStorage.getItem("isLogin") === "true" ? this.loggedInCard() : this.loggedOutCard()} 
        </>               
      );    
  }
}
```

After fetching the articles from the `/articles` endpoint, we will iterate the `ArticleCard` component in our Home Page.

```typescript
render() {      
      return (
        <>   
        <Header title="Home"></Header>
        <IonContent> 
          <IonSegment onIonChange={this.toggle} color="success">
              <IonSegmentButton value="Global" color="success" checked={this.state.segment==='Global' }>
                  <IonLabel>Global Feed</IonLabel>
              </IonSegmentButton>
              {localStorage.getItem("isLogin") === "true" ? <IonSegmentButton value="myfeed" color="success"
                  checked={this.state.segment==='myfeed' }>
                  <IonLabel>Your Feed</IonLabel>
              </IonSegmentButton> : '' }
          </IonSegment>
        <IonList>
        {this.state.articles.map((article: any) => 
        <ArticleCard key={article.slug} title={article.title} src={article.author.image} description={article.description} favorited={article.favorited} favoritesCount={article.favoritesCount} slug={article.slug} author={article.author.username}></ArticleCard>
        )}      
        </IonList>
        <TagCloud onTagClick={(e: any) => this.handleTagClick(e)} ></TagCloud>   
        </IonContent>    
      </>
      );
    }

```
The `toggle` and `handleTagClick` functions are in the source code. 

### Article Page
We are fetching an individual article by `slug` in the article page. The body of the article is in Markdown format. To display the article, we will be using showdown to convert it. Additionally, comments for the article are fetched from the `/articles/slug/comments` endpoint and logged in users can post a comment. We will be creating a separate `Comment` component to iterate over the list. To post comments, logged in users can use `IonTextArea`.

```shell
npm i showdown @types/showdown 
```

We will initialize showdown using:

```
  this.converter = new Showdown.Converter({
          tables: true,
          simplifiedAutoLink: true,
          strikethrough: true,
          tasklists: true,
          requireSpaceBeforeHeadingText: true
        });  

```
And we will display markdown display using: 

```
   <div dangerouslySetInnerHTML={{ __html: this.converter.makeHtml(article.body)}}></div>

            </div>
```

The rest of the source can be viewed [here](https://github.com/JscramblerBlog/conduit-ionic-react).

<a href="https://blog.jscrambler.com/protecting-your-react-js-source-code-with-jscrambler/?utm_source=dev.to&utm_medium=referral" target="_blank"><img alt="Protect your React App with Jscrambler" src="https://media.jscrambler.com/blog/V6-React.jpg"></a>

### Login Page 

Login Page is a basic form Page. We will be accepting email and password. The `/users/login` endpoint accepts the POST request and returns a JWT token. We will save the JWT token in localstorage to manage our auth. In order to toggle the side menu, we will be using standard JS events. Also, we will be signing up users using the `/users` endpoint. Additionally, we are using the `IonToast` component to display error messages. The code used for the Login Page is below. `IonInput` uses `CustomEvent` and we can use the `onIonChange` event to bind the input to the state.

```typescript
updateUserName = (event: any) => {
    this.setState({ username: event.detail.value });  
  };
login= () => {
    let url , credentials;     
    if(this.state.action  == 'Login'){
      url = CONFIG.API_ENDPOINT + '/users/login';
      credentials = {
        "user": {
          "email": this.state.email,
          "password": this.state.password
      }
      }

    } else {
      url = CONFIG.API_ENDPOINT + '/users';
      credentials = {
        "user": {
          "email": this.state.email,
          "password": this.state.password,
          "username": this.state.username
      }
      }
    }
    fetch(url, {
            method: 'POST',
            headers: {
                "Content-Type": "application/json",              
            },
            body: JSON.stringify(credentials)

        })
        .then((res) => {
          console.log(res);
          if(res.status == 200){
            return res.json();
          } else {  
            if(this.state.action == 'SignUp') {
              throw new Error("Error creating user");
            } else {
              throw new Error("Error Logging in")  
            }                
          }
         
        } )
        .then(
          (result) => {
              console.log(result);    
              localStorage.setItem("token",result.user.token);       
              localStorage.setItem("username", result.user.username);
              localStorage.setItem("isLogin", "true");
              localStorage.setItem("email", result.user.email);
             
              this.event = new CustomEvent('loggedIn', {
                detail: true
              });
              window.dispatchEvent(this.event);
              this.props.history.push('/blog');
          },
    
          (error) => {
           console.error(error);           
           this.setState({toastMessage: error.toString(), toastState: true});
          }
        )
  }

  render(){
    return(
      <>
    <Header title="Login"></Header>    
    <IonContent padding>

    <div className="ion-text-center">
    <img src={image} alt="logo" width="25%" /> 
    </div>
    <h1 className="ion-text-center conduit-title">conduit</h1>      

    <IonToast
        isOpen={this.state.toastState}
        onDidDismiss={() => this.setState(() => ({ toastState: false }))}
        message= {this.state.toastMessage}
        duration={400}
      >
      </IonToast>
        
    <form action="">
    
    <IonItem>
      
      <IonInput  onIonChange={this.updateEmail} type="email" placeholder="Email"></IonInput>
    </IonItem>
    {this.state.action === 'SignUp' ?    
      <IonItem>
       
        <IonInput onIonChange={this.updateUserName} type="text" placeholder="Username"></IonInput>
      </IonItem>
      : <></> }
    
    <IonItem>
    <IonInput onIonChange={this.updatePassword} type="password" placeholder="Password"></IonInput>      
    </IonItem>

    </form>      
    
            <IonButton onClick={this.login}>{this.state.action}</IonButton>         
         
    </IonContent>
    <IonFooter>
      <IonToolbar text-center>
          Click here to <a onClick={this.toggleAction}>{this.state.action === 'Login'? 'SignUp' : 'Login'}</a>
      </IonToolbar>
    </IonFooter>
  </>
    )
  }
```

### New Article Page

This page is a form page like the login page. However, for the article body, we will be using a Markdown editor instead of a text area. For this tutorial, we are going to use [react-mde](https://github.com/andrerpena/react-mde). 

```
npm install --save react-mde
```

The form code is as below:

```HTML
<form onSubmit={this.submitArticle}>
      <IonInput type="text" placeholder="Title" onIonChange={this.titleChange} class="border-input"></IonInput>
    <IonInput type="text" placeholder="What's this article about"  onIonChange={this.descriptionChange} class="border-input"></IonInput>
  
          <ReactMde
          onChange={this.handleBodyChange}
          onTabChange={this.handleTabChange}
          value={this.state.articleBody}
          selectedTab={this.state.tab}
          generateMarkdownPreview={markdown =>
            Promise.resolve(this.converter.makeHtml(markdown))
          }
        />
         <IonInput type="text"  placeholder="Enter Tags" class="border-input"  onIonChange={this.tagsChange}></IonInput>
         <IonButton expand="block" onClick={this.submitArticle}>Submit Article</IonButton>        
      </form>    
```

### Creating a Build Using Capacitor

We will use `run build` to build our React project:

```
npm run build
```
We will then use Capacitor to copy the files:

```
npx capacitor copy
```
Now, we will need to compile the project natively:

```
npx capacitor open android|ios
```

## Final Thoughts

My mileage while developing Conduit varied a lot. Conduit doesn't have a spec for mobile, so it took a while to figure out the best workflow.

We can improve the app by using middleware like [Redux](https://blog.jscrambler.com/becoming-familiar-with-redux/), using skeletons loaders at places, integrating infinite scroll and handling errors better. To complete the app experience, we will have to modify the backend so it can handle push notifications. A notification system makes sense if you receive a new follower or if a person you are following publishes a new post.

As far as Ionic React is concerned, it felt like I was developing a React web application using a set of predefined components. Components like `IonInfiniteScroll` are still WIP and we can hope to get them soon. Personally, I missed the tight integration of the framework and the side menu template. We can expect full Ionic CLI and Cordova support in the future.

Android users can check the app [here](https://play.google.com/store/apps/details?id=me.karandpr.conduit).

**Lastly, if you're building an application with sensitive logic, be sure to protect it against code theft and reverse-engineering by following our guides for <a href="https://blog.jscrambler.com/protecting-your-react-js-source-code-with-jscrambler/?utm_source=dev.to&utm_medium=referral" target="_blank">React</a> and for <a href="https://blog.jscrambler.com/protecting-hybrid-mobile-apps-with-ionic-and-jscrambler/?utm_source=dev.to&utm_medium=referral" target="_blank">Ionic</a>.**

Originally published by Karan Gandhi in the [Jscrambler Blog](https://blog.jscrambler.com/).