gauravbytes.com is now codefoundry.dev.
Showing posts with label Typescript. Show all posts

Introduction

React was first introducted to general public in May 2013; roughly three years after the first release of Angular JS (October 2010). Soon, it picked up the momentum and now is the highest stared(~150K) and forked(29.2K) repository on Github. The positive point of React with its contemporary libraries was the backward compatibility in all the released versions. It started as a class based library (extending React.Component) to pure functional library with React Hooks; still keeping backward compatibility. Now, new features include asyncronous rendering with Suspense. React's ecosystem is very vast with lots of frameworks available to choose from. We will start with building first simple a.ka. Welcome react application and then build full-stack application with React, Redux, reselect, Next.JS, express JS and Node.JS. So, stay tuned :)

Building your first React application

You can create React application with project like create-react-app or can create customize project intialiting the project with npm and then pick and choose libraries of your choice. In this post, we will use create-react-app.

create-react-app conviently configures the tools like webpack, babel and testing libraries, so that you can concentrate purely on application code.

npx create-react-app my-first-react-app

npx is a package runner tool that comes with npm 5.2+ and higher.

This single line of code will setup Javascript based project, configures webpack, babel and testing libraries.

my-first-react-app
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── serviceWorker.js
    └── setupTests.js

It will also configure commands in package.json to start, build, test and eject (A command to remove transitive dependencies of webpack, babel, testing libraries and copies directly to package.json so that you can customize accordingly).

Let's run the application with npm run start and visit localhost:3000 on browser.

Creating first React component

Let's start with creating a component.

Create a new file Welcome.js and Welcome.css under src folder.

Add following lines to Welcome.js

import React from 'react';
import './Welcome.css';

function Welcome() {
  return <div className='welcome'>Welcome! My first react app</div>
}

export default Welcome;

Here, we have created a functional component which is equivalent to extending React.Component class and adding render function in it.

import React from 'react';
import './Welcome.css';

class Welcome extends React.Component {
  render() {
    return <div className='welcome'>Welcome! My first react app</div>
  }
}

export default Welcome;

Now, the first React component; let's add it to App.js

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <Welcome />
      </header>
    </div>
  );
}

Now, go to localhost:3000 and you will see the component loaded.

Creating Typescript based first React application

Typescript is a typed superset of Javascript which compiles to plain Javascript. It provides the type safety to Javascript. create-react-app provides convenient way to change template for generating React skeleton project. Just pass the template parameter with value as follows.

npx create-react-app my-first-react-app --template typescript

Adding first typed component

Create a new file Welcome.tsx and Welcome.css under src folder.

Add following lines to Welcome.tsx

import React from 'react';
import './Welcome.css';

interface IWelcomeProps {
  message?: string
}

function Welcome(props: IWelcomeProps) {
  const message = props?.message ?? 'Welcome! My first React app with Typescript.'
  return (<div className='welcome'>{message}</div>);
}

export default Welcome;

We have defined the interface IWelcomeProps with single optional field message. In the functional component, we have used the Nullish coalescing added in Typescript 3.7.

Let's add this component to App.tsx

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <Welcome />
        <Welcome message="Welcome Reader! My first react app with Typescript."/>
      </header>
    </div>
  );
}

We have added Welcome twice; with and without message field. The output after running the application will look like this.

Recap

We created our first React application using create-react-app project. We added first React component Welcome.js and Welcome.tsx in Javascript and Typescript based projects respectively.

What's next?

In the next post, we will build a BlogPost application using React's functional component and React hooks. We will use useState hook for state management. So, stay tuned!

Building a Blog Post application

Let's create a blog post application. It will have below features:

  • Option to search blog posts.
  • Option to list blog posts.
  • Option to show blog post.

Create a new project with npx create-react-app react-blog-posts --template typescript.

Create BlogPosts.tsx component under src/components folder and IBlogPost model under src/models.

import React from 'react';
import IBlogPost from '../models/IBlogPost';

interface IBlogPostsProps {
  posts: Array<IBlogPost>
}


function BlogPosts(props: IBlogPostsProps) {
  return (
    <div className="blog-container">
      <ul className="blog-posts">
        {
          props.posts.map(post => <li key={post.id}>{post.title}</li>)
        }
      </ul>
    </div>
  );
}

export default BlogPosts;
interface IBlogPost {
  id: number
  title: string
  content: string
  author: string
  postedOn: string
  tags: string[]
}

export default IBlogPost;

Explanation: We have created BlogPosts function which takes parameter of type IBlogPostsProps. This type contains array of posts of type IBlogPost. We are only showing title of the Blog Post in this component. Shortly, we will update this component and extract listing of BlogPosts as seperate component. For now, Let's update App.tsx and use BlogPosts to show dummy posts.

function App() {
  return (
    <div className="App-Container">
      <BlogPosts posts={POSTS}/>
    </div>
  );
}

You can get the dummy posts array from here

Run the application npm run start and you will see the page loaded with post titles.

Now, Let's create a new component BlogPost.tsx which will show the selected blog post.

import React from 'react';
import IBlogPost from '../models/IBlogPost';
import './BlogPost.css';

interface IBlogPostProps {
  post: IBlogPost
}

function BlogPost(props: IBlogPostProps) {
  const post = props.post
  return (
    <div className='blog-post'>
      <div className='blog-post-title'>{post.title}</div>
      <div className='blog-post-body'>{post.content}</div>
      <div className='blog-post-footer'>
        <div className='blog-author'>{`By ${post.author} at ${post.postedOn}`}</div>
        <div className='blog-tags'>
          <div key='tags-label'>Tags: </div>
          {post.tags.map(tag => <div key={tag}>{tag}</div>)}
        </div>
      </div>
    </div>
  );
}

export default BlogPost;

Create a new component BlogListing.tsx to list the available posts to read.

import React from 'react';

declare type IBlogPostData = {
  id: number
  title: string
}

interface IBlogListing {
  blogPosts: IBlogPostData[]
  selectedBlogPost: number
  onClick: (id: number) => void
}

function BlogListing(props: IBlogListing) {
  return(
    <div className='blog-listing'>
      <ul className="blog-posts">
        {
          props.blogPosts.map(post => <li className={props.selectedBlogPost === post.id ? 'active' : ''} key={post.id} onClick={() => props.onClick(post.id)}>{post.title}</li>)
        }
      </ul>
    </div>
  );
}

export default BlogListing;

In this component, we have declared IBlogPostData as type which holds id and title of the blog to be listed. This component takes collection of posts, selectedBlogPost(active post) and onClick function (action to perform when link is clicked) as arguments.

Now, update BlogPosts.tsx component and use BlogListing and BlogPost in it.

function BlogPosts(props: IBlogPostsProps) {
  /*1.*/const firsBlogPost = props.posts && props.posts.length > 0 ? props.posts[0] : null;
  /*2.*/const [ selectedBlogPost, setSelectedBlogPost ] = useState<IBlogPost | null>(firsBlogPost);

  /*3.*/function onBlogPostLinkClick(id: number): void {
    const selectedBlogPost = props.posts.find(post => post.id === id);
    setSelectedBlogPost(!!selectedBlogPost ? selectedBlogPost : null);
  }

  return (
    <div className="blog-container">
      <BlogListing
        selectedBlogPost={selectedBlogPost?.id ?? 0}
        blogPosts={props.posts.map(post => { return {id: post.id, title: post.title }})}
        /*4.*/onClick={onBlogPostLinkClick}
      />
      {!!selectedBlogPost ? <BlogPost post={selectedBlogPost}/>: null }
    </div>
  );
}

export default BlogPosts;

Explanation: At line 1, we retrieve the first post from list of posts passed in this component. In line 2, We are using React hook useState for local state management. We are using this to mamange state for selected post to be shown in BlogPost.tsx component. At line 3, we declared a function which updates the selectedBlogPost in local state. At line 4, we are passing onBlogPostLinkClick function as an argument to BlogListing.tsx. This function will get called when you click on the any of the post link in BlogListing.tsx component.

Run the application npm run start and you will see the page loaded with first post as selected as shown in below screenshot.

Now, we will add option to search blog posts either by title or tags. Create a component BlogSearch.tsx under src/components folder.

import React, { ChangeEvent } from 'react';
import { SearchType } from '../models/SearchType';

interface IBlogSearchProps {
  searchText: string
  selectedSearchOn: string
  onSearchChange: (searchText: string, searchType: SearchType) => void
  onSearchButtonClick: () => void
}

function BlogSearch(props: IBlogSearchProps) {
  function onSearchTextChange(event: ChangeEvent<HTMLInputElement>): void {
    props.onSearchChange(event.target.value, SearchType.SEARCH_TEXT)
  }

  function onSearchOnChange(event: ChangeEvent<HTMLSelectElement>): void {
    props.onSearchChange(event.target.value, SearchType.SEARCH_ON)
  }

  return(
    <div className="blog-search-container">
      <div className='blog-search-title'>Search Blog</div>
      <div className='blog-search-body'>
        <input type="text" className="form-control" autoComplete="off" value={props?.searchText ?? ''} onChange={onSearchTextChange}/>
        <select value={props.selectedSearchOn} className='form-control' onChange={onSearchOnChange}>
          <option value='tag'>Tags</option>
          <option value='title'>Title</option>
        </select>
        <button type="button" className="form-button" onClick={props.onSearchButtonClick}>Search</button>
      </div>
    </div>
  );
}

export default BlogSearch;

Explanation: This component expects four properties; searchText (text to be searched), selectedSearchOn(Whether it is tag or title search) and two functions one for whenever there is a change in the Search Text or Search On fields and other function for when Search button is clicked. These functions are passed on from top component BlogPosts.tsx because we are doing local state management in that component and all other components are stateless.

We also updated BlogListing.tsx to use BlogSearch.tsx component. We also changed this component; it takes the four more properties used by BlogSearch.tsx component. Finally, we have updated BlogPosts.tsx component.

import React, { useState } from 'react';
import IBlogPost from '../models/IBlogPost';
import './BlogPosts.css';
import BlogListing from './BlogListing';
import BlogPost from './BlogPost';
import { SearchType } from '../models/SearchType';

interface IBlogPostsProps {
  posts: Array<IBlogPost>
}


function BlogPosts(props: IBlogPostsProps) {
  function findFirstPost(posts: Array<IBlogPost>) : IBlogPost | null {
    return posts && posts.length > 0 ? posts[0] : null;
  }

  /*1.*/const [ posts, setPosts ] = useState(props.posts)
  /*2.*/const [ showingPost, setShowingPost ] = useState<IBlogPost | null>(findFirstPost(posts));
  /*3.*/const [ searchText, setSearchText ] = useState<string>('');
  /*4.*/const [ selectedSearchOn, setSelectedSearchOn ] = useState<string>('tag')

  /*5.*/function onBlogPostLinkClick(id: number): void {
    const newShowingPost = posts.find(post => post.id === id);
    setShowingPost(!!newShowingPost ? newShowingPost : null);
  }
  
  /*6.*/function onChangeHandler(value: string, searchType: SearchType) : void {
   if (SearchType.SEARCH_TEXT === searchType) {
    setSearchText(value)
   } else {
     setSelectedSearchOn(value)
   }
  }

  function isMatched(value: string) {
    return value.toLowerCase().includes(searchText.toLowerCase())
  }

  function filterPost(post: IBlogPost) {
    if (selectedSearchOn === 'title') {
      return isMatched(post.title)
    } else {
      return post.tags.some(isMatched)
    }
  }

  /*7.*/function onSearch() {
    if (searchText !== '') {
      const foundPosts = props.posts.filter(filterPost)
      setShowingPost(findFirstPost(foundPosts))
      setPosts(foundPosts)
    } else {
      setShowingPost(findFirstPost(props.posts))
      setPosts(props.posts)
    }
  }

  return (
    <div className="blog-container">
      <BlogListing
        showingPost={showingPost?.id ?? 0}
        blogPosts={posts.map(post => { return {id: post.id, title: post.title }})}
        onClick={onBlogPostLinkClick}
        searchText={searchText}
        onSearchChange={onChangeHandler}
        onSearchButtonClick={onSearch}
        selectedSearchOn={selectedSearchOn}
      />
      {!!showingPost ? <BlogPost post={showingPost}/>: null }
    </div>
  );
}

export default BlogPosts;

Line 1 to 4 declare posts, showingPost, searchText and selectedSearchOn respectively.

Line 5 defines onClick function whenever post link is clicked on BlogListing component. This function takes the blog id to be shown and search in the list of posts (see Line 1) in local state and updates the showingPost(see Line 2) field in the local state.

Line 6 defines a onChange function which get called whenever searchtext or searchon field is changing on BlogSearch component.Based on search type, it either updates searchText (see line 3) or selectedSearchOn (see line 4) in local state.

Line 7 defines onClick function for Search button on BlogSearch component. This function updates the posts (see line 1) and showingPost (see line 2) in the local state based on searchText (see line 3) and selectedSearchOn (See line 4) fields in the local state.

Recap

We used create-react-app module to create first React project (Javascript and Typescript based). Then, we added first React component (Welcome.js and Welcome.tsx) in the projects. We started building a blog website which have functionality to list posts, search posts and show post. Then, We created BlogPosts.tsx which was only showing the name of posts. Then, we created two components BlogListing.tsx to show the list of posts and BlogPost.tsx to show the currently viewing post. Then, we added statement management in BlogPosts.tsx to show the post whenever post link is clicked in BlogListing.tsx component. Next, we added BlogSearch.tsx component to search blog based on Title or Tags.

What's next?

In the next post, we will introduce Redux to manage the state and reselect to add selector in the application. Stay tuned!.

Note: You can download the final source code for this application from github.