Let's create a blog post application. It will have below features:
Create a new project with npx create-react-app react-blog-posts --template typescript
command.
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 typeIBlogPostsProps
. This type contains array of posts of typeIBlogPost
. We are only showing title of the Blog Post in this component. Shortly, we will update this component and extract listing ofBlogPosts
as seperate component. For now, Let's updateApp.tsx
and useBlogPosts
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;
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.
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.
- At 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 inBlogPost.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 toBlogListing.tsx
. This function will get called when you click on the any of the post link inBlogListing.tsx
component.
npm run start
and you will see the page loaded with first post as selected as shown in below screenshot.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 useBlogSearch.tsx
component. We also changed this component; it takes the four more properties used byBlogSearch.tsx
component. Finally, we have updatedBlogPosts.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
andselectedSearchOn
respectively.Line 5 defines
onClick
function whenever post link is clicked onBlogListing
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 onBlogSearch
component. Based on search type, it either updatessearchText
(see line 3) orselectedSearchOn
(see line 4) in local state.Line 7 defines
onClick
function for Search button onBlogSearch
component. This function updates the posts (see line 1) andshowingPost
(see line 2) in the local state based onsearchText
(see line 3) andselectedSearchOn
(See line 4) fields in the local state.
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.
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.