├── .eslintrc.json ├── .gitignore ├── README.md ├── components └── Table │ ├── index.tsx │ └── styled.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ ├── api.d.ts │ └── users.ts ├── columns.tsx └── index.tsx ├── public ├── favicon.ico └── vercel.svg └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the project I created for my [blog](https://dev.to/serhatgenc/creating-a-reusable-table-component-with-react-table-and-material-ui-10jd) post about react-table. 2 | -------------------------------------------------------------------------------- /components/Table/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Paper, 4 | Skeleton, 5 | Table as MuiTable, 6 | TableHead, 7 | TableRow, 8 | TableCell, 9 | TableBody, 10 | TextField, 11 | } from "@mui/material"; 12 | import { 13 | Cell, 14 | ColumnDef, 15 | flexRender, 16 | getCoreRowModel, 17 | Row, 18 | useReactTable, 19 | } from "@tanstack/react-table"; 20 | import { debounce } from "lodash"; 21 | import { ChangeEvent, FC, memo, useMemo, useState } from "react"; 22 | import { StyledPagination, StyledTableRow } from "./styled"; 23 | 24 | interface TableProps { 25 | data: any[]; 26 | columns: ColumnDef[]; 27 | isFetching?: boolean; 28 | skeletonCount?: number; 29 | skeletonHeight?: number; 30 | headerComponent?: JSX.Element; 31 | pageCount?: number; 32 | page?: (page: number) => void; 33 | search?: (search: string) => void; 34 | onClickRow?: (cell: Cell, row: Row) => void; 35 | searchLabel?: string; 36 | } 37 | 38 | const Table: FC = ({ 39 | data, 40 | columns, 41 | isFetching, 42 | skeletonCount = 10, 43 | skeletonHeight = 28, 44 | headerComponent, 45 | pageCount, 46 | search, 47 | onClickRow, 48 | page, 49 | searchLabel = "Search", 50 | }) => { 51 | const [paginationPage, setPaginationPage] = useState(1); 52 | 53 | const memoizedData = useMemo(() => data, [data]); 54 | const memoizedColumns = useMemo(() => columns, [columns]); 55 | const memoisedHeaderComponent = useMemo( 56 | () => headerComponent, 57 | [headerComponent] 58 | ); 59 | 60 | const { getHeaderGroups, getRowModel, getAllColumns } = useReactTable({ 61 | data: memoizedData, 62 | columns: memoizedColumns, 63 | getCoreRowModel: getCoreRowModel(), 64 | manualPagination: true, 65 | pageCount, 66 | }); 67 | 68 | const skeletons = Array.from({ length: skeletonCount }, (x, i) => i); 69 | 70 | const columnCount = getAllColumns().length; 71 | 72 | const noDataFound = 73 | !isFetching && (!memoizedData || memoizedData.length === 0); 74 | 75 | const handleSearchChange = ( 76 | e: ChangeEvent 77 | ) => { 78 | search && search(e.target.value); 79 | }; 80 | 81 | const handlePageChange = ( 82 | event: ChangeEvent, 83 | currentPage: number 84 | ) => { 85 | setPaginationPage(currentPage === 0 ? 1 : currentPage); 86 | page?.(currentPage === 0 ? 1 : currentPage); 87 | }; 88 | 89 | return ( 90 | 91 | 92 | {memoisedHeaderComponent && {memoisedHeaderComponent}} 93 | {search && ( 94 | 101 | )} 102 | 103 | 104 | 105 | {!isFetching && ( 106 | 107 | {getHeaderGroups().map((headerGroup) => ( 108 | 109 | {headerGroup.headers.map((header) => ( 110 | 111 | {header.isPlaceholder 112 | ? null 113 | : flexRender( 114 | header.column.columnDef.header, 115 | header.getContext() 116 | )} 117 | 118 | ))} 119 | 120 | ))} 121 | 122 | )} 123 | 124 | {!isFetching ? ( 125 | getRowModel().rows.map((row) => ( 126 | 127 | {row.getVisibleCells().map((cell) => ( 128 | onClickRow?.(cell, row)} 130 | key={cell.id} 131 | > 132 | {flexRender( 133 | cell.column.columnDef.cell, 134 | cell.getContext() 135 | )} 136 | 137 | ))} 138 | 139 | )) 140 | ) : ( 141 | <> 142 | {skeletons.map((skeleton) => ( 143 | 144 | {Array.from({ length: columnCount }, (x, i) => i).map( 145 | (elm) => ( 146 | 147 | 148 | 149 | ) 150 | )} 151 | 152 | ))} 153 | 154 | )} 155 | 156 | 157 | 158 | {noDataFound && ( 159 | 160 | No Data Found 161 | 162 | )} 163 | {pageCount && page && ( 164 | 170 | )} 171 | 172 | ); 173 | }; 174 | 175 | export default memo(Table); 176 | -------------------------------------------------------------------------------- /components/Table/styled.ts: -------------------------------------------------------------------------------- 1 | import { Pagination, styled, TableRow } from "@mui/material"; 2 | 3 | export const StyledTableRow = styled(TableRow)` 4 | &:nth-of-type(odd) { 5 | background-color: #f1f1f1; 6 | } 7 | &:last-child td, 8 | &:last-child th { 9 | border: 0; 10 | } 11 | :hover { 12 | background-color: #d9d9d9; 13 | } 14 | `; 15 | 16 | export const StyledPagination = styled(Pagination)` 17 | display: flex; 18 | justify-content: center; 19 | margin-top: 1rem; 20 | `; 21 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reusable-react-table", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.9.3", 13 | "@emotion/styled": "^11.9.3", 14 | "@mui/icons-material": "^5.8.4", 15 | "@mui/material": "^5.8.7", 16 | "@tanstack/react-query": "^4.0.5", 17 | "@tanstack/react-query-devtools": "^4.0.10", 18 | "@tanstack/react-table": "^8.2.3", 19 | "axios": "^0.27.2", 20 | "lodash": "^4.17.21", 21 | "next": "12.2.0", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0" 24 | }, 25 | "devDependencies": { 26 | "@types/lodash": "^4.14.182", 27 | "@types/node": "18.0.1", 28 | "@types/react": "18.0.14", 29 | "@types/react-dom": "18.0.6", 30 | "eslint": "8.19.0", 31 | "eslint-config-next": "12.2.0", 32 | "typescript": "4.7.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | import CssBaseline from "@mui/material/CssBaseline"; 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | 6 | const queryClient = new QueryClient(); 7 | 8 | function MyApp({ Component, pageProps }: AppProps) { 9 | return ( 10 | 11 | 12 |
13 | 14 | 15 |
16 |
17 | ); 18 | } 19 | 20 | export default MyApp; 21 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | DocumentContext, 3 | DocumentInitialProps, 4 | Head, 5 | Html, 6 | Main, 7 | NextScript, 8 | } from "next/document"; 9 | 10 | class MyDocument extends Document { 11 | static async getInitialProps( 12 | ctx: DocumentContext 13 | ): Promise { 14 | const initialProps = await Document.getInitialProps(ctx); 15 | 16 | return initialProps; 17 | } 18 | render() { 19 | return ( 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | ); 35 | } 36 | } 37 | 38 | export default MyDocument; 39 | -------------------------------------------------------------------------------- /pages/api/api.d.ts: -------------------------------------------------------------------------------- 1 | declare module Api { 2 | declare module Users { 3 | export interface FetchUsersResponse { 4 | data: Api.Users.Data[]; 5 | maxPageSize: number; 6 | } 7 | export interface Data { 8 | id: number; 9 | name: string; 10 | email: string; 11 | gender: string; 12 | status: string; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pages/api/users.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | try { 9 | const { data, headers } = await axios.get( 10 | "https://gorest.co.in/public/v2/users", 11 | { 12 | params: req.query, 13 | } 14 | ); 15 | return res.status(200).json({ 16 | data, 17 | maxPageSize: 18 | Number(headers["x-pagination-pages"]) === 0 19 | ? 1 20 | : Number(headers["x-pagination-pages"]), 21 | }); 22 | } catch (error) { 23 | return res.status(404); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pages/columns.tsx: -------------------------------------------------------------------------------- 1 | import { Chip } from "@mui/material"; 2 | import { ColumnDef } from "@tanstack/react-table"; 3 | 4 | export const columns: ColumnDef[] = [ 5 | { 6 | accessorKey: "name", 7 | header: "Name", 8 | }, 9 | { 10 | accessorKey: "email", 11 | header: "Email", 12 | }, 13 | { 14 | accessorKey: "gender", 15 | header: "Gender", 16 | }, 17 | { 18 | accessorKey: "status", 19 | header: "Status", 20 | cell: (row: any) => { 21 | return ( 22 | 27 | ); 28 | }, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Box, Button, Typography } from "@mui/material"; 2 | import type { NextPage } from "next"; 3 | import axios from "axios"; 4 | import Table from "../components/Table"; 5 | import { columns } from "./columns"; 6 | import { useQuery } from "@tanstack/react-query"; 7 | import { useState } from "react"; 8 | 9 | const Home: NextPage = () => { 10 | const [currentPage, setCurrentPage] = useState(1); 11 | const [search, setSearch] = useState(""); 12 | 13 | const fetchUsers = async () => { 14 | const params = { 15 | ...(!search && { page: currentPage }), 16 | ...(search && { name: search }), 17 | }; 18 | 19 | const { data } = await axios.get( 20 | "/api/users", 21 | { 22 | params, 23 | } 24 | ); 25 | return data; 26 | }; 27 | 28 | const { data, isFetching, isError, error, isSuccess } = useQuery< 29 | Api.Users.FetchUsersResponse, 30 | Error 31 | >(["users", currentPage, search], fetchUsers, { 32 | refetchOnWindowFocus: false, 33 | keepPreviousData: true, 34 | }); 35 | 36 | const onClickRow = (cell: any, row: any) => { 37 | console.log({ cell, row }); 38 | }; 39 | 40 | const Header = ( 41 | 42 | 43 | User Table 44 | 45 | 46 | 47 | ); 48 | 49 | return ( 50 | 51 | {isError && {error?.message}} 52 | {isSuccess && ( 53 | 64 | )} 65 | 66 | ); 67 | }; 68 | 69 | export default Home; 70 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serhatgnc/reusable-react-table/895bfc5a95c5273304f41c464b4d502cb88dfc01/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------