Web Development

Developing a Real World App in Ionic React

May 30th, 2019 | By Karan Gandhi | 11 min read

A complete tutorial to build a Conduit - a Medium clone Real-World App - using React.

Ionic recently added support for the React and Vue frameworks.

Traditionally, Ionic used Angular and Cordova frameworks to create their apps. With Ionic 4, we can develop framework-agnostic apps.

Learn how to create a basic Ionic app in the React Framework.

RealWorld and Conduit

The folks at Thinkster are behind a project known as the RealWorld project. Conduit is a Medium clone that can be built using multiple stacks. We can mix and match different types of backends and frontends. Every backend and frontend follows the same API 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 the Article Page

    • Navigate to the 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 the Author Page

  • Settings Page (Auth required)

    • Modify user settings

  • Profile Page (With and Without Auth)

    • Display User details

    • Display Posts by the user

    • Display posts Favorited by the 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. We will use that endpoint for this tutorial. Alternatively, you can set up your 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 the 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.

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.

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 logged-in key and an Event Listener are used to manage user state. The token obtained from authentication will be saved in localstorage.

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;

Side menu Component:

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 with an array of 20 articles. A normal user can:

  • View a list of articles

  • Navigate to a full article

  • Navigate to author's 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, the Header is a wrapper around the Ionic Header with the title as props.

   <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.

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.

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.

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.

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 the user state.

  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.

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.

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.

npm install --save react-mde


The form code is as below:

<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


Conclusion

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, using skeleton 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.

Jscrambler

The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.

View All Articles

Must read next

Web Development

Background Services in Ionic Capacitor

Capacitor provides a different approach from Cordova, by focusing on a native development workflow. Here, we explore how it handles background tasks.

October 29, 2019 | By Karan Gandhi | 5 min read

Web Development

Seamless App Updates with CodePush in React Native and Ionic

CodePush enables seamlessly managing partial application updates without relying on app store releases. Learn how to set it up for React Native and Ionic.

July 10, 2019 | By Karan Gandhi | 5 min read

Section Divider