React with Typescript 🔥
Author : JaNakh Pon , August 11, 2021
Tags
Intro
In this article, we are going to build a react app with CRUD functions, pagination, search and sort.
md/lg/xl screen (Desktop/Laptop/Tablet)

xs/sm screen (Mobile)

Setup
We are going to use the setup form the previous repo from React and TailwindCSS Article. So, Let's get started by creating folders for Components(InputComponent, PaginationComponent, TasksComponent), Types(types we would need props, api calls) and utils(helper functions):
cd src && mkdir Components Types utils
Types
First and foremost, let's create a Types/task.types.ts
file:
export type TaskType = {
id: number,
title: string,
text: string,
completed: boolean,
created_at: string,
updated_at: string
}
export type SearchResponse = {
data: TaskType[],
count: number
}
export type CreateTask = {
title: string,
text: string
}
export type CountType = {
count: number
}
export enum SortType {
A = "ASC",
D = "DESC"
}
Components
cd Components && mkdir InputComponent Pagination Tasks
InputComponent
InputComponent will have an input textfield for creating new task and searchbox for searching and two selectboxes for sorting.
import { ChangeEvent, FormEvent } from 'react'
import { CreateTask } from '../../Types/task.type'
type InputProps = {
search: string;
task: CreateTask;
handleSave: (v: FormEvent) => void;
handleChange: (v: ChangeEvent) => void;
setSearch: (v: string) => void;
handleSearch: (e: FormEvent) => void;
setSort: (v: string) => void;
handleOrder: (v: string) => void;
}
const InteractiveInputComponent = ({ task, handleSave, handleChange, search, setSearch, handleSearch, setSort, handleOrder }: InputProps) => {
return (
<>
<div className="flex justify-center m-5">
<section className="w-10/12 sm:w-10/11 lg:w-1/2 max-w-2xl flex flex-col items-center">
<form className="flex justify-between w-full" onSubmit={handleSave}>
<input type="text" placeholder={`write things down here ...`} value={task.text} onChange={handleChange} />
</form>
</section>
</div>
<div className="flex justify-center m-5">
<section className="w-10/12 sm:w-10/11 lg:w-1/2 max-w-2xl flex flex-col items-center">
<form className="flex justify-between w-full" onSubmit={handleSearch}>
<div className=" w-full relative text-gray-700">
<input className="w-full h-10 pl-8 pr-3" type="text" placeholder="search here .." value={search} onChange={(e) => setSearch(e.target.value)} />
<div className="absolute inset-y-0 left-0 flex items-center px-2 pointer-events-none">
<svg className="w-4 h-4 fill-current" viewBox="0 0 20 20">
<path d="M18.125,15.804l-4.038-4.037c0.675-1.079,1.012-2.308,1.01-3.534C15.089,4.62,12.199,1.75,8.584,1.75C4.815,1.75,1.982,4.726,2,8.286c0.021,3.577,2.908,6.549,6.578,6.549c1.241,0,2.417-0.347,3.44-0.985l4.032,4.026c0.167,0.166,0.43,0.166,0.596,0l1.479-1.478C18.292,16.234,18.292,15.968,18.125,15.804 M8.578,13.99c-3.198,0-5.716-2.593-5.733-5.71c-0.017-3.084,2.438-5.686,5.74-5.686c3.197,0,5.625,2.493,5.64,5.624C14.242,11.548,11.621,13.99,8.578,13.99 M16.349,16.981l-3.637-3.635c0.131-0.11,0.721-0.695,0.876-0.884l3.642,3.639L16.349,16.981z"></path>
</svg>
</div>
</div>
<select onChange={(e) => setSort(e.target.value)}>
<option className="p-1">updated_at</option>
<option className="p-1">created_at</option>
<option className="p-1">id</option>
<option className="p-1">title</option>
</select>
<select onChange={(e) => handleOrder(e.target.value)}>
<option className="p-1">ASC</option>
<option className="p-1">DESC</option>
</select>
</form>
</section>
</div>
</>
)
}
export default InteractiveInputComponent;
The first flex container will be for input text field and the second flex container will be using for the search box and select boxes placed in a row.
TasksComponent
TasksComponent will be a container to display a list of tasks. There are two .ts files inside /Task/
, the first file:index.tsx will be using to get props from the main:App.tsx and loop through each task and the second file:item.tsx is a component that display detailed data for each item.
import { ChangeEvent } from 'react'
import Task from './item'
import { TaskType } from '../../Types/task.type'
type TasksProps = {
isrsearch: boolean;
searchcount: number;
searchedTasks: TaskType[];
tasks: TaskType[];
handleComplete: (e: ChangeEvent, task: TaskType) => void;
handleDelete: (i: number) => void
}
const DisplayTasks = ({ isrsearch, searchcount, tasks, searchedTasks, handleComplete, handleDelete }: TasksProps) => {
return (
<div className="grid grid-flow-row auto-rows-max lg:mt-5 sm:mt-2">
{
isrsearch ? searchcount ? (<>
<div className="flex justify-center mx-8">
<section className="w-10/12 sm:w-10/11 lg:w-1/2 max-w-2xl">
<p>Found {searchcount} tasks!</p>
</section>
</div>
{
!!searchedTasks.length && searchedTasks.map((t, i) => {
return <Task key={i} task={t} handleComplete={handleComplete} handleDelete={handleDelete} />
})
}
</>) : (<div className="flex justify-center mx-8">
<section className="w-10/12 sm:w-10/11 lg:w-1/2 max-w-2xl">
<p>No tasks found with the current search term!</p>
</section>
</div>) : (<>
{
!!tasks.length && tasks.map((t, i) => {
return <Task key={i} task={t} handleComplete={handleComplete} handleDelete={handleDelete} />
})
}
</>)
}
</div>
)
}
export default DisplayTasks
and
import { ChangeEvent } from 'react'
import { TaskType } from '../../Types/task.type'
type TaskProps = {
task: TaskType;
handleComplete: (e: ChangeEvent, task: TaskType) => void;
handleDelete: (i: number) => void
}
const Task = ({ task, handleComplete, handleDelete }: TaskProps) => {
return (
<div className="flex justify-center mt-10 mx-8">
<section className="w-10/12 sm:w-10/11 lg:w-1/2 max-w-2xl">
<h2 className="font-bold ml-2">{task.title}, <span className="font-light text-sm">{task.created_at}</span></h2>
<input type="checkbox" checked={task.completed} onChange={(e) => handleComplete(e, task)} />
<span className="text-left text-sm leading-3 tracking-wider px-1">{task.text} | <span className="cursor-pointer hover:uppercase hover:font-bolder hover:text-red-500" onClick={() => handleDelete(task.id)}> remove?</span></span>
</section>
</div>
)
}
export default Task
Pagination
Pagination Component will be a row within a flex container that loop over the pages. Just like TasksComponent, /Pagination/
will also need an item component to use when looping pages.
import PagItem from './item'
type PaginationProps = {
search: string;
isrsearch: boolean;
searchcount: number;
page: number;
topagfs: number;
topag: number;
handlePagination: (i: number) => void
}
const Pagination = ({ isrsearch, search, searchcount, page, topagfs, topag, handlePagination }: PaginationProps) => {
return (
<div className="flex justify-center mt-10 mx-8">
<section className="w-10/12 sm:w-10/11 lg:w-1/2 max-w-2xl flex justify-center">
<nav aria-label="Page navigation">
<ul className="inline-flex space-x-2">
{
isrsearch&&search !== "" ? searchcount ? ([...Array(topagfs)].map((val, i) => {
return <PagItem i={i} page={page} handlePagination={handlePagination} key={`${val} + ${i}`} />
})) : null : (
[...Array(topag)].map((val, i) => {
return <PagItem i={i} page={page} handlePagination={handlePagination} key={`${val} + ${i}`} />
})
)
}
</ul>
</nav>
</section>
</div>
)
}
export default Pagination
and
type PagItemProps = {
i: number;
page: number;
handlePagination: (i: number) => void
}
const PagItem = ({ i, page, handlePagination }: PagItemProps) => {
return (
<li>
<button className={`w-10 h-10 ${i + 1 === page ? " text-white transition-colors duration-150 bg-gray-500 border border-r-0 border-gray-500 rounded-full focus:shadow-outline" : "transition-colors duration-150 rounded-full focus:shadow-outline hover:bg-gray-100"} `} onClick={() => handlePagination(i + 1)}>{i + 1}</button>
</li>
)
}
export default PagItem
utils
Now, it's time to define some helper functions to use in App.tsx
before we fetch/manipulate data and render it through the Components we created above!:
// let's export it out as URL to make it short looks nicer on network requests
export const URL = `http://localhost:3001/api/v1/todos`
const isFloat = (n: number) => {
return Number(n) === n && n % 1 !== 0;
}
// pass count and get pages
export const calculatePagination = (count: number) => {
let condition = 0
if (count !== 0) {
if (isFloat(count / 5)) {
let x = count / 5;
let decimals = x - Math.floor(x);
condition = x - decimals + 1;
} else {
condition = count / 5;
}
}
return condition
}
// just a placholder for title field when creating new task by text
export const days = ["Sunday", "Monday", "Tuesday", "Wednesday ", "Thursday", "Friday", "Saturday"]
CRUD with axios
We will be using our previous nest api with postgres from the previous article, so make sure that the backend server is running 😉.
Now, let's talk about "axios with react hooks", we will use useEffect() with GET request for data fetching
, and update/delete/post requests
will be triggered on action events especially for creating new task, removing task and checking/unchecking checkbox for updating task.
However, for searching and sorting we will need to use both hooks and action events.
import { ChangeEvent, FormEvent, useState, useEffect } from 'react'
import axios from 'axios'
import Pagination from './Components/Pagination'
import DisplayTasks from './Components/Tasks'
import InteractiveInputComponent from './Components/InputComponent'
import { TaskType, SearchResponse, CreateTask, CountType, SortType } from './Types/task.type'
import { calculatePagination, URL, days } from './utils/task.common'
const App = () => {
// a bunch of functions with hooks and based on action events to CRUD data
// Now we got data from api requests, so pass it to the components
// also pass functions based on events to the components
// Don't worry, clone the repo from the source code link and review it!
return (
<div className="container mx-auto lg:my-32 md:my-30 sm:my-15 ">
<InteractiveInputComponent handleSave={handleSave} handleChange={handleChange} task={task} search={search} setSearch={setSearch} setSort={setSort} handleSearch={handleSearch} handleOrder={handleOrder} />
<DisplayTasks isrsearch={isrsearch} searchcount={searchcount} tasks={tasks} searchedTasks={searchedTasks} handleComplete={handleComplete} handleDelete={handleDelete} />
<Pagination isrsearch={isrsearch} search={search} searchcount={searchcount} page={page} topag={topag} topagfs={topagfs} handlePagination={handlePagination} />
</div>
);
}
export default App;
Not very much of an explaination right? 😹 well, don't be sad!, reviewing other ppl source code without explaination will make you a genius sooner or later 😜😉. Source Code.
Go Back.