Skip to main content

Your First App using Chakra UI

Introduction

This tutorial will go through process of building a simple admin panel for a CMS-like application with headless.

Step by step, you're going to learn how to consume a REST API and add basic CRUD functionality to your panel leveraging the unique capabilities of refine.

Let's begin by setting up a new refine project.

Setting up

There are two alternative methods to set up a refine application.

The recommended way is using the create refine-app tool. create refine-app's CLI wizard will let you create and customize your application in seconds.

Alternatively, you may use the create-react-app tool to create an empty React application and then add refine module via npm.

First, run the create refine-app with the following command:

npm create refine-app -- -o refine-chakra-ui tutorial

About Fake REST API

refine is designed to consume data from APIs.

For the sake of this tutorial, we will provide you a fully working, fake REST API located at https://api.fake-rest.refine.dev/. You may take a look at available resources and routes of the API before proceeding to the next step.

Using a Dataprovider

Dataproviders are refine components making it possible to consume different API's and data services conveniently. To consume our Fake REST API, we'll use the "Simple REST Dataprovider".

Next, navigate to the project folder and run the following command to install the required package:

npm i @pankod/refine-simple-rest
note

If you used create refine-app to bootstrap the project, you can skip issuing this command as create refine-app already installs the selected data provider.

note

Fake REST API is based on JSON Server Project. Simple REST Dataprovider is fully compatible with the REST rules and methods of the JSON Server.

Bootstrapping the Application

Replace the contents of App.tsx with the following code:

src/App.tsx
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import dataProvider from "@pankod/refine-simple-rest";
import {
ChakraProvider,
ErrorComponent,
Layout,
refineTheme,
ReadyPage,
notificationProvider,
} from "@pankod/refine-chakra-ui";

const App = () => {
return (
<ChakraProvider theme={refineTheme}>
<Refine
notificationProvider={notificationProvider()}
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
Layout={Layout}
ReadyPage={ReadyPage}
catchAll={<ErrorComponent />}
/>
</ChakraProvider>
);
};
info

Refine application uses Montserrat font by default as it is defined in the typography property of the theme. But to use Montserrat, you need to embed it to your index.html file. For more information about adding font family in your Refine application, you can look at Chakra UI Theme Customization.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;500;600;700;800;900&display=swap"
rel="stylesheet"
/>
<title>refine adding font family example</title>
</head>

<body>
...
</body>
</html>
tip

refine comes native with Light/Dark theme support. Check out the theme documentation for more information.


<Refine/> is the root component of a refine application. Using the dataProvider prop, we made our Simple REST Dataprovider available to the entire application.

Run the following command to launch the app in development mode:

npm run dev

Your refine application should be up and running!
Point your browser to http://localhost:3000 to access it. You will see the welcome page.

http://localhost:3000

Adding Resources

Now we are ready to start connecting to our API by adding a resource to our application.

Let's add /posts/ endpoint from our API as a resource. First take a look to the raw API response for the request made to the /posts/ route:

Show response

GET https://api.fake-rest.refine.dev/posts/
[
{
"id": 1,
"title": "Eius ea autem sapiente placeat fuga voluptas quos quae.",
"slug": "beatae-esse-dolor",
"content": "Explicabo nihil delectus. Nam aliquid sunt numquam...",
"category": {
"id": 24
},
"user": {
"id": 7
},
"status": "draft",
"createdAt": "2021-03-13T03:09:30.186Z",
"image": [],
"tags": [
7,
4
],
"language": 2
},
...
]


Now, add the highlighted code to your App.tsx to connect to the endpoint.

src/App.tsx
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import dataProvider from "@pankod/refine-simple-rest";
import {
ChakraProvider,
ErrorComponent,
Layout,
refineTheme,
ReadyPage,
notificationProvider,
} from "@pankod/refine-chakra-ui";

const App = () => {
return (
<ChakraProvider theme={refineTheme}>
<Refine
notificationProvider={notificationProvider()}
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
Layout={Layout}
ReadyPage={ReadyPage}
catchAll={<ErrorComponent />}
resources={[{ name: "posts" }]}
/>
</ChakraProvider>
);
};
info

resources is a property of <Refine/> representing API Endpoints. The name property of every single resource should match one of the endpoints in your API!

Instead of showing the welcome page, the application should redirect now to an URL defined by the name property. Open your application to check that the URL is routed to /posts:

http://localhost:3000/posts

You'll still see a 404 error page because no Page component is assigned to our resource yet.

note

resources use Page components to handle data and perform rendering. Page components are passed to resources as an array of objects. For basic CRUD operations, there are four predefined props: list, create, edit and show.

Let's create a Page component to fetch posts and display them as a table. Later, we will pass the component as the list prop to our resource.

Creating a List Page

First, we'll need an interface to work with the data from the API endpoint.

tip

We'll use the @pankod/refine-react-table for benefit of the TanStack Table v8 library. However, you can use useTable without the @pankod/refine-react-table package.

Create a new folder named "interfaces" under "/src" if you don't already have one. Then create a "index.d.ts" file with the following code:

interfaces/index.d.ts
export interface IPost {
id: number;
title: string;
status: "published" | "draft" | "rejected";
createdAt: string;
}

We'll be using title, status and createdAt fields of every post record.

Now, create a new folder named "pages/posts" under "/src". Under that folder, create a "list.tsx" file with the following code:

src/pages/posts/list.tsx
import React from "react";
import { useTable, ColumnDef, flexRender } from "@pankod/refine-react-table";
import {
List,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
HStack,
Text,
DateField,
} from "@pankod/refine-chakra-ui";

import { IPost } from "../../interfaces";

export const PostList: React.FC = () => {
const columns = React.useMemo<ColumnDef<IPost>[]>(
() => [
{
id: "id",
header: "ID",
accessorKey: "id",
},
{
id: "title",
header: "Title",
accessorKey: "title",
},
{
id: "status",
header: "Status",
accessorKey: "status",
},
{
id: "createdAt",
header: "Created At",
accessorKey: "createdAt",
cell: function render({ getValue }) {
return (
<DateField value={getValue() as string} format="LLL" />
);
},
},
],
[],
);

const { getHeaderGroups, getRowModel } = useTable({
columns,
refineCoreProps: {
initialSorter: [
{
field: "id",
order: "desc",
},
],
},
});

return (
<List>
<TableContainer whiteSpace="pre-line">
<Table variant="simple">
<Thead>
{getHeaderGroups().map((headerGroup) => (
<Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Th key={header.id}>
{!header.isPlaceholder && (
<HStack spacing="2">
<Text>
{flexRender(
header.column.columnDef
.header,
header.getContext(),
)}
</Text>
</HStack>
)}
</Th>
))}
</Tr>
))}
</Thead>
<Tbody>
{getRowModel().rows.map((row) => (
<Tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<Td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</List>
);
};

@pankod/refine-react-table hook uses useTable() fetches data from API. Normally, TanStack-table's useReactTable expects a data prop but @pankod/refine-react-table's useTable doesn't expect a data prop.

Refer to the @pankod/refine-react-table for more information. →

note

We didn't use arrow functions for rendering cell because of the react/display-name is not compatible with arrow functions. If you want to use arrow functions, you can use like this:

pages/posts/list.tsx
// eslint-disable-next-line
renderCell: ({ getValue }) => (
<DateField value={getValue() as string} format="LLL" />
);

Refer to <DateField /> for more information.


Finally, we are ready to add <PostList> to our resource. Add the highlighted line to your App.tsx:

src/App.tsx
import { Refine } from "@pankod/refine-core";
import {
ChakraProvider,
ErrorComponent,
Layout,
refineTheme,
ReadyPage,
notificationProvider,
} from "@pankod/refine-chakra-ui";
import dataProvider from "@pankod/refine-simple-rest";
import routerProvider from "@pankod/refine-react-router-v6";

import { PostList } from "./pages";

const App: React.FC = () => {
return (
<ChakraProvider theme={refineTheme}>
<Refine
notificationProvider={notificationProvider()}
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
Layout={Layout}
ReadyPage={ReadyPage}
catchAll={<ErrorComponent />}
resources={[{ name: "posts", list: PostList }]}
/>
</ChakraProvider>
);
};

Note you will need a few more files which help src/App.tsx to find your pages and posts. In the /pages folder, put this index.tsx file in it which allows everything in the posts folder to be used elsewhere.

src/pages/index.ts
export * from "./posts";

Similarly, put a file in the /src/pages/posts folder which accomplishes the same function. We will use the commented out code later as we add more capabilities to our app. Remember as you add functions, uncomment each appropriate line.

src/pages/posts/index.ts
export * from "./list";

Open your application in your browser. You will see posts are displayed correctly in a table structure and even the pagination works out-of-the box.

On the next step, we are going to add a category field to the table which involves handling data relationships.

http://localhost:3000/posts

Handling relationships

Remember the records from /posts endpoint that had a category id field.

https://api.fake-rest.refine.dev/posts/1
...
"category": {
"id": 26
}
...

To display category titles on our table, we need to category id to their corresponding titles. The category title data can be obtained from the /categories endpoint for each record.

https://api.fake-rest.refine.dev/categories/26
  {
"id": 26,
"title": "mock category title",
}

At this point, we need to join records from different resources. For this, we're going to use the refine hook useMany.

Before we start, just edit our interface for the new ICategory type:

interfaces/index.d.ts
export interface ICategory {
id: number;
title: string;
}

export interface IPost {
id: number;
title: string;
status: "published" | "draft" | "rejected";
category: { id: number };
createdAt: string;
}

So we can update our list.tsx with the highlighted lines:

src/pages/posts/list.tsx
import React from "react";
import { useTable, ColumnDef, flexRender } from "@pankod/refine-react-table";
import {
List,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
HStack,
Text,
DateField,
} from "@pankod/refine-chakra-ui";
import { GetManyResponse, useMany } from "@pankod/refine-core";

import { IPost, ICategory } from "../../interfaces";

export const PostList: React.FC = () => {
const columns = React.useMemo<ColumnDef<IPost>[]>(
() => [
{
id: "id",
header: "ID",
accessorKey: "id",
},
{
id: "title",
header: "Title",
accessorKey: "title",
},
{
id: "status",
header: "Status",
accessorKey: "status",
},
{
id: "category.id",
header: "Category",
enableColumnFilter: false,
accessorKey: "category.id",
cell: function render({ getValue, table }) {
const meta = table.options.meta as {
categoriesData: GetManyResponse<ICategory>;
};
const category = meta.categoriesData?.data.find(
(item) => item.id === getValue(),
);
return category?.title ?? "Loading...";
},
},
{
id: "createdAt",
header: "Created At",
accessorKey: "createdAt",
cell: function render({ getValue }) {
return (
<DateField value={getValue() as string} format="LLL" />
);
},
},
],
[],
);

const {
getHeaderGroups,
getRowModel,
setOptions,
refineCore: {
tableQueryResult: { data: tableData },
},
} = useTable({
columns,
});

const categoryIds = tableData?.data?.map((item) => item.category.id) ?? [];
const { data: categoriesData } = useMany<ICategory>({
resource: "categories",
ids: categoryIds,
queryOptions: {
enabled: categoryIds.length > 0,
},
});

setOptions((prev) => ({
...prev,
meta: {
...prev.meta,
categoriesData,
},
}));

return (
<List>
<TableContainer whiteSpace="pre-line">
<Table variant="simple">
<Thead>
{getHeaderGroups().map((headerGroup) => (
<Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Th key={header.id}>
{!header.isPlaceholder && (
<HStack spacing="2">
<Text>
{flexRender(
header.column.columnDef
.header,
header.getContext(),
)}
</Text>
</HStack>
)}
</Th>
))}
</Tr>
))}
</Thead>
<Tbody>
{getRowModel().rows.map((row) => (
<Tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<Td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</List>
);
};

We construct an array of categoryId's from /posts endpoint and pass it to the useMany hook. categoriesData will be filled with id-title tuples to be used for rendering our component.

Try the result on your browser and you'll notice that the category column is filled correctly with the matching category titles for the each record's category id's. Even the loading state is managed by refine.

To get more detailed information about this hook, please refer the useMany Documentation.

http://localhost:3000/posts

Adding Sorting and Filtering

The @pankod/refine-react-table package also listens for changes in filters and sorting states of the Tanstack Table and updates the table accordingly. The change in these states triggers the fetch of the new data.

So, we can add filters and sorting features to our table as suggested by TanStack Table with the following code:

src/pages/posts/list.tsx
import { useTable, ColumnDef, flexRender } from "@pankod/refine-react-table";
import {
List,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
HStack,
Text,
DateField,
Select,
} from "@pankod/refine-chakra-ui";
import { GetManyResponse, useMany } from "@pankod/refine-core";

import { ColumnFilter, ColumnSorter } from "../../components/table";
import { IPost, ICategory, FilterElementProps } from "../../interfaces";

export const PostList: React.FC = () => {
const columns = React.useMemo<ColumnDef<IPost>[]>(
() => [
{
id: "id",
header: "ID",
accessorKey: "id",
enableColumnFilter: false,
},
{
id: "title",
header: "Title",
accessorKey: "title",
meta: {
filterOperator: "contains",
},
},
{
id: "status",
header: "Status",
accessorKey: "status",
meta: {
filterElement: function render(props: FilterElementProps) {
return (
<Select defaultValue="published" {...props}>
<option value="published">Published</option>
<option value="draft">Draft</option>
<option value="rejected">Rejected</option>
</Select>
);
},
filterOperator: "eq",
},
},
{
id: "category.id",
header: "Category",
enableColumnFilter: false,
accessorKey: "category.id",
cell: function render({ getValue, table }) {
const meta = table.options.meta as {
categoriesData: GetManyResponse<ICategory>;
};
const category = meta.categoriesData?.data.find(
(item) => item.id === getValue(),
);
return category?.title ?? "Loading...";
},
},
{
id: "createdAt",
header: "Created At",
accessorKey: "createdAt",
enableColumnFilter: false,
cell: function render({ getValue }) {
return (
<DateField value={getValue() as string} format="LLL" />
);
},
},
],
[],
);

const {
getHeaderGroups,
getRowModel,
setOptions,
refineCore: {
setCurrent,
pageCount,
current,
tableQueryResult: { data: tableData },
},
} = useTable({
columns,
});

const categoryIds = tableData?.data?.map((item) => item.category.id) ?? [];
const { data: categoriesData } = useMany<ICategory>({
resource: "categories",
ids: categoryIds,
queryOptions: {
enabled: categoryIds.length > 0,
},
});

setOptions((prev) => ({
...prev,
meta: {
...prev.meta,
categoriesData,
},
}));

return (
<List>
<TableContainer whiteSpace="pre-line">
<Table variant="simple">
<Thead>
{getHeaderGroups().map((headerGroup) => (
<Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Th key={header.id}>
{!header.isPlaceholder && (
<HStack spacing="2">
<Text>
{flexRender(
header.column.columnDef
.header,
header.getContext(),
)}
</Text>
<HStack spacing="2">
<ColumnSorter
column={header.column}
/>
<ColumnFilter
column={header.column}
/>
</HStack>
</HStack>
)}
</Th>
))}
</Tr>
))}
</Thead>
<Tbody>
{getRowModel().rows.map((row) => (
<Tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<Td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</List>
);
};
src/interfaces/index.d.ts
export interface IPost {
id: number;
title: string;
status: "published" | "draft" | "rejected";
category: { id: number };
createdAt: string;
}

export interface ICategory {
id: number;
title: string;
}

export interface ColumnButtonProps {
column: Column<any, any>;
}

export interface FilterElementProps {
value: any;
onChange: (value: any) => void;
}
Show ColumnFilter
src/components/table/columnFilter.tsx
import React, { useState } from "react";
import {
Input,
Menu,
IconButton,
MenuButton,
MenuList,
VStack,
HStack,
} from "@pankod/refine-chakra-ui";
import { IconFilter, IconX, IconCheck } from "@tabler/icons";

import { ColumnButtonProps } from "../../interfaces";

export const ColumnFilter: React.FC<ColumnButtonProps> = ({ column }) => {
// eslint-disable-next-line
const [state, setState] = useState(null as null | { value: any });

if (!column.getCanFilter()) {
return null;
}

const open = () =>
setState({
value: column.getFilterValue(),
});

const close = () => setState(null);

// eslint-disable-next-line
const change = (value: any) => setState({ value });

const clear = () => {
column.setFilterValue(undefined);
close();
};

const save = () => {
if (!state) return;
column.setFilterValue(state.value);
close();
};

const renderFilterElement = () => {
// eslint-disable-next-line
const FilterComponent = (column.columnDef?.meta as any)?.filterElement;

if (!FilterComponent && !!state) {
return (
<Input
borderRadius="md"
size="sm"
autoComplete="off"
value={state.value}
onChange={(e) => change(e.target.value)}
/>
);
}

return (
<FilterComponent
value={state?.value}
onChange={(e: any) => change(e.target.value)}
/>
);
};

return (
<Menu isOpen={!!state} onClose={close}>
<MenuButton
onClick={open}
as={IconButton}
aria-label="Options"
icon={<IconFilter size="16" />}
variant="ghost"
size="xs"
/>
<MenuList p="2">
{!!state && (
<VStack align="flex-start">
{renderFilterElement()}
<HStack spacing="1">
<IconButton
aria-label="Clear"
size="sm"
colorScheme="red"
onClick={clear}
>
<IconX size={18} />
</IconButton>
<IconButton
aria-label="Save"
size="sm"
onClick={save}
colorScheme="green"
>
<IconCheck size={18} />
</IconButton>
</HStack>
</VStack>
)}
</MenuList>
</Menu>
);
};
Show ColumnSorter
src/components/table/columnSorter.tsx
import { IconButton } from "@chakra-ui/react";
import { IconChevronDown, IconSelector } from "@tabler/icons";

import { ColumnButtonProps } from "../../interfaces";

export const ColumnSorter: React.FC<ColumnButtonProps> = ({ column }) => {
if (!column.getCanSort()) {
return null;
}

const sorted = column.getIsSorted();

return (
<IconButton
aria-label="Sort"
size="xs"
onClick={column.getToggleSortingHandler()}
style={{
transition: "transform 0.25s",
transform: `rotate(${sorted === "asc" ? "180" : "0"}deg)`,
}}
variant={sorted ? "light" : "transparent"}
color={sorted ? "primary" : "gray"}
>
{sorted ? (
<IconChevronDown size={18} />
) : (
<IconSelector size={18} />
)}
</IconButton>
);
};
http://localhost:3000/posts

Pagination

Chakra UI does not provide a pagination component. Let's create a simple pagination component.

Show Pagination Component
src/components/pagination/index.tsx
import { FC } from "react";
import { HStack, Button, Box } from "@chakra-ui/react";
import { IconChevronRight, IconChevronLeft } from "@tabler/icons";
import { IconButton, usePagination } from "@pankod/refine-chakra-ui";

type PaginationProps = {
current: number;
pageCount: number;
setCurrent: (page: number) => void;
};

export const Pagination: FC<PaginationProps> = ({
current,
pageCount,
setCurrent,
}) => {
const pagination = usePagination({
current,
pageCount,
});

return (
<Box display="flex" justifyContent="flex-end">
<HStack my="3" spacing="1">
{pagination?.prev && (
<IconButton
aria-label="previous page"
onClick={() => setCurrent(current - 1)}
disabled={!pagination?.prev}
variant="outline"
>
<IconChevronLeft size="18" />
</IconButton>
)}

{pagination?.items.map((page) => {
if (typeof page === "string")
return <span key={page}>...</span>;

return (
<Button
key={page}
onClick={() => setCurrent(page)}
variant={page === current ? "solid" : "outline"}
>
{page}
</Button>
);
})}
{pagination?.next && (
<IconButton
aria-label="next page"
onClick={() => setCurrent(current + 1)}
variant="outline"
>
<IconChevronRight size="18" />
</IconButton>
)}
</HStack>
</Box>
);
};
src/pages/posts/list.tsx
import { useTable, ColumnDef, flexRender } from "@pankod/refine-react-table";
import {
List,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
HStack,
Text,
DateField,
Select,
} from "@pankod/refine-chakra-ui";
import { GetManyResponse, useMany } from "@pankod/refine-core";

import { ColumnFilter, ColumnSorter } from "../../components/table";
import { Pagination } from "../../components/pagination";
import { IPost, ICategory, FilterElementProps } from "../../interfaces";

export const PostList: React.FC = () => {
const columns = React.useMemo<ColumnDef<IPost>[]>(
() => [
{
id: "id",
header: "ID",
accessorKey: "id",
},
{
id: "title",
header: "Title",
accessorKey: "title",
meta: {
filterOperator: "contains",
},
},
{
id: "status",
header: "Status",
accessorKey: "status",
meta: {
filterElement: function render(props: FilterElementProps) {
return (
<Select defaultValue="published" {...props}>
<option value="published">Published</option>
<option value="draft">Draft</option>
<option value="rejected">Rejected</option>
</Select>
);
},
filterOperator: "eq",
},
},
{
id: "category.id",
header: "Category",
enableColumnFilter: false,
accessorKey: "category.id",
cell: function render({ getValue, table }) {
const meta = table.options.meta as {
categoriesData: GetManyResponse<ICategory>;
};
const category = meta.categoriesData?.data.find(
(item) => item.id === getValue(),
);
return category?.title ?? "Loading...";
},
},
{
id: "createdAt",
header: "Created At",
accessorKey: "createdAt",
cell: function render({ getValue }) {
return (
<DateField value={getValue() as string} format="LLL" />
);
},
enableColumnFilter: false,
},
],
[],
);

const {
getHeaderGroups,
getRowModel,
setOptions,
refineCore: {
tableQueryResult: { data: tableData },
setCurrent,
pageCount,
current,
},
} = useTable({
columns,
});

const categoryIds = tableData?.data?.map((item) => item.category.id) ?? [];
const { data: categoriesData } = useMany<ICategory>({
resource: "categories",
ids: categoryIds,
queryOptions: {
enabled: categoryIds.length > 0,
},
});

setOptions((prev) => ({
...prev,
meta: {
...prev.meta,
categoriesData,
},
}));

return (
<List>
<TableContainer whiteSpace="pre-line">
<Table variant="simple">
<Thead>
{getHeaderGroups().map((headerGroup) => (
<Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Th key={header.id}>
{!header.isPlaceholder && (
<HStack spacing="2">
<Text>
{flexRender(
header.column.columnDef
.header,
header.getContext(),
)}
</Text>
<HStack spacing="2">
<ColumnSorter
column={header.column}
/>
<ColumnFilter
column={header.column}
/>
</HStack>
</HStack>
)}
</Th>
))}
</Tr>
))}
</Thead>
<Tbody>
{getRowModel().rows.map((row) => (
<Tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<Td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Pagination
current={current}
pageCount={pageCount}
setCurrent={setCurrent}
/>
</List>
);
};
http://localhost:3000/posts

Showing a single record

At this point we are able to list all post records on the table component with pagination, sorting and filtering functionality. Next, we are going to add a details page to fetch and display data from a single record.

Let's create a <PostShow> component on /pages/posts folder:

/pages/posts/show.tsx
import { useShow, useOne } from "@pankod/refine-core";
import {
Show,
Heading,
Text,
MarkdownField,
Spacer,
} from "@pankod/refine-chakra-ui";

import { ICategory, IPost } from "../../interfaces";

export const PostShow: React.FC = () => {
const { queryResult } = useShow<IPost>();
const { data, isLoading } = queryResult;
const record = data?.data;

const { data: categoryData } = useOne<ICategory>({
resource: "categories",
id: record?.category.id || "",
queryOptions: {
enabled: !!record?.category.id,
},
});

return (
<Show isLoading={isLoading}>
<Heading as="h5" size="sm">
Id
</Heading>
<Text mt={2}>{record?.id}</Text>

<Heading as="h5" size="sm" mt={4}>
Title
</Heading>
<Text mt={2}>{record?.title}</Text>

<Heading as="h5" size="sm" mt={4}>
Status
</Heading>
<Text mt={2}>{record?.status}</Text>

<Heading as="h5" size="sm" mt={4}>
Category
</Heading>
<Text mt={2}>{categoryData?.data?.title}</Text>

<Heading as="h5" size="sm" mt={4}>
Content
</Heading>
<Spacer mt={2} />
<MarkdownField value={record?.content} />
</Show>
);
};

✳️ useShow() is a refine hook used to fetch a single record of data. The queryResult has the response and also isLoading state.

Refer to the useShow documentation for detailed usage information.

✳️ To retrieve the category title, again we need to make a call to /categories endpoint. This time we used useOne() hook to get a single record from another resource.

Refer to the useOne documentation for detailed usage information.

attention

useShow() is the preferred hook for fetching data from the current resource. To query foreign resources you may use the low-level useOne() hook.

Since we've got access to raw data returning from useShow(), there is no restriction on how it's displayed on your components. If you prefer presenting your content with a nicer wrapper, refine provides you the <Show> component which has extra features like list and refresh buttons.

Refer to the <Show> documentation for detailed usage information.


Now we can add the newly created component to our resource with show prop:

src/App.tsx
import { Refine } from "@pankod/refine-core";
import {
ChakraProvider,
ErrorComponent,
Layout,
refineTheme,
ReadyPage,
notificationProvider,
} from "@pankod/refine-chakra-ui";
import dataProvider from "@pankod/refine-simple-rest";
import routerProvider from "@pankod/refine-react-router-v6";

import { PostList, PostShow } from "./pages";

const App: React.FC = () => {
return (
<ChakraProvider theme={refineTheme}>
<Refine
notificationProvider={notificationProvider()}
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
Layout={Layout}
ReadyPage={ReadyPage}
catchAll={<ErrorComponent />}
resources={[{ name: "posts", list: PostList, show: PostShow }]}
/>
</ChakraProvider>
);
};

And then we can add a <ShowButton> on the list page to make it possible for users to navigate to detail pages:

src/pages/posts/list.tsx
import {
...
ShowButton,
} from "@pankod/refine-chakra-ui";

const PostList: React.FC = () => {
const columns = React.useMemo<ColumnDef<IPost>[]>(
() => [
...
{
id: "actions",
header: "Actions",
accessorKey: "id",
enableColumnFilter: false,
enableSorting: false,
cell: function render({ getValue }) {
return (
<ShowButton
hideText
recordItemId={getValue() as number}
/>
);
},
},
],
[],
);


const { ... } = useTable<IPost>({ columns });

return (
...
);
};
http://localhost:3000/posts

Editing a record

Until this point, we were basically working with reading operations such as fetching and displaying data from resources. From now on, we are going to start creating and updating records by using useForm.

Let's start by creating a new <PostEdit> page responsible for editing a single record:

src/pages/posts/edit.tsx
import { useEffect } from "react";
import {
Edit,
FormControl,
FormErrorMessage,
FormLabel,
Input,
Select,
} from "@pankod/refine-chakra-ui";
import { useSelect } from "@pankod/refine-core";
import { useForm } from "@pankod/refine-react-hook-form";

import { IPost } from "../../interfaces";

export const PostEdit = () => {
const {
refineCore: { formLoading, queryResult },
saveButtonProps,
register,
formState: { errors },
resetField,
} = useForm<IPost>();

const { options } = useSelect({
resource: "categories",
defaultValue: queryResult?.data?.data.category.id,
queryOptions: { enabled: !!queryResult?.data?.data.category.id },
});

useEffect(() => {
resetField("category.id");
}, [options]);

return (
<Edit isLoading={formLoading} saveButtonProps={saveButtonProps}>
<FormControl mb="3" isInvalid={!!errors?.title}>
<FormLabel>Title</FormLabel>
<Input
id="title"
type="text"
{...register("title", { required: "Title is required" })}
/>
<FormErrorMessage>
{`${errors.title?.message}`}
</FormErrorMessage>
</FormControl>
<FormControl mb="3" isInvalid={!!errors?.status}>
<FormLabel>Status</FormLabel>
<Select
id="content"
placeholder="Select Post Status"
{...register("status", {
required: "Status is required",
})}
>
<option>published</option>
<option>draft</option>
<option>rejected</option>
</Select>
<FormErrorMessage>
{`${errors.status?.message}`}
</FormErrorMessage>
</FormControl>
<FormControl mb="3" isInvalid={!!errors?.categoryId}>
<FormLabel>Category</FormLabel>
<Select
id="category"
placeholder="Select Category"
{...register("category.id", {
required: true,
})}
>
{options?.map((option) => (
<option value={option.value} key={option.value}>
{option.label}
</option>
))}
</Select>
<FormErrorMessage>
{`${errors.categoryId?.message}`}
</FormErrorMessage>
</FormControl>
</Edit>
);
};

Let's see what's going on our <PostEdit> component in detail:

✳️ useForm is a refine hook for handling form data. In edit page, useForm hook initializes the form with current record values.

Attention

✳️ <Input> is Chakra UI components to build form inputs.

✳️ Save button submits the form by executing the useUpdate method provided by the dataProvider. After a successful response, the application will be redirected to the listing page.

Now we can add the newly created component to our resource with edit prop:

src/App.tsx
import { Refine } from "@pankod/refine-core";
import {
ChakraProvider,
ErrorComponent,
Layout,
refineTheme,
ReadyPage,
notificationProvider,
} from "@pankod/refine-chakra-ui";
import dataProvider from "@pankod/refine-simple-rest";
import routerProvider from "@pankod/refine-react-router-v6";

import { PostList, PostShow, PostEdit } from "./pages";

const App: React.FC = () => {
return (
<ChakraProvider theme={refineTheme}>
<Refine
notificationProvider={notificationProvider()}
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
Layout={Layout}
ReadyPage={ReadyPage}
catchAll={<ErrorComponent />}
resources={[
{
name: "posts",
list: PostList,
show: PostShow,
edit: PostEdit,
},
]}
/>
</ChakraProvider>
);
};

We are going to need an edit button on each row to display the <PostEdit> component. refine doesn't automatically add one, so we have to update our <PostList> component to add a <EditButton> for each record:

src/pages/posts/list.tsx
import {
...
ShowButton,
EditButton
HStack,
} from "@pankod/refine-chakra-ui";

const PostList: React.FC = () => {
const columns = React.useMemo<ColumnDef<IPost>[]>(
() => [
...
{
id: "actions",
header: "Actions",
accessorKey: "id",
enableColumnFilter: false,
enableSorting: false,
cell: function render({ getValue }) {
return (
<HStack>
<ShowButton
hideText
size="sm"
recordItemId={getValue() as number}
/>
<EditButton
hideText
size="sm"
recordItemId={getValue() as number}
/>
</HStack>
);
},
},
],
[],
);


const { ... } = useTable<IPost>({ columns });

return (
...
);
};

Refer to the <EditButton> documentation for detailed usage information.

You can try using edit buttons which will trigger the edit forms for each record, allowing you to update the record data.

http://localhost:3000/posts

Creating a record

Creating a record in refine follows a similar flow as editing records.

First, we'll create a <PostCreate> page:

src/pages/posts/create.tsx
import {
Create,
FormControl,
FormErrorMessage,
FormLabel,
Input,
Select,
} from "@pankod/refine-chakra-ui";
import { useSelect } from "@pankod/refine-core";
import { useForm } from "@pankod/refine-react-hook-form";

import { IPost } from "../../interfaces";

export const PostCreate = () => {
const {
refineCore: { formLoading },
saveButtonProps,
register,
formState: { errors },
} = useForm<IPost>();

const { options } = useSelect({
resource: "categories",
});

return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<FormControl mb="3" isInvalid={!!errors?.title}>
<FormLabel>Title</FormLabel>
<Input
id="title"
type="text"
{...register("title", { required: "Title is required" })}
/>
<FormErrorMessage>
{`${errors.title?.message}`}
</FormErrorMessage>
</FormControl>
<FormControl mb="3" isInvalid={!!errors?.status}>
<FormLabel>Status</FormLabel>
<Select
id="content"
placeholder="Select Post Status"
{...register("status", {
required: "Status is required",
})}
>
<option>published</option>
<option>draft</option>
<option>rejected</option>
</Select>
<FormErrorMessage>
{`${errors.status?.message}`}
</FormErrorMessage>
</FormControl>
<FormControl mb="3" isInvalid={!!errors?.categoryId}>
<FormLabel>Category</FormLabel>
<Select
id="categoryId"
placeholder="Select Category"
{...register("categoryId", {
required: "Category is required",
})}
>
{options?.map((option) => (
<option value={option.value} key={option.value}>
{option.label}
</option>
))}
</Select>
<FormErrorMessage>
{`${errors.categoryId?.message}`}
</FormErrorMessage>
</FormControl>
</Create>
);
};

We should notice some minor differences from the edit example:

✳️ <form> is wrapped with <Create> component.

✳️ Save button submits the form by executing the useCreate method provided by the dataProvider.

✳️ No defaultValue is passed to useSelect.


After creating the <PostCreate> component, add it to resource with create prop:


src/App.tsx
import { Refine } from "@pankod/refine-core";
import {
ChakraProvider,
ErrorComponent,
Layout,
refineTheme,
ReadyPage,
notificationProvider,
} from "@pankod/refine-chakra-ui";
import dataProvider from "@pankod/refine-simple-rest";
import routerProvider from "@pankod/refine-react-router-v6";

import { PostList, PostShow, PostEdit, PostCreate } from "./pages";

const App: React.FC = () => {
return (
<ChakraProvider theme={refineTheme}>
<Refine
notificationProvider={notificationProvider()}
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
Layout={Layout}
ReadyPage={ReadyPage}
catchAll={<ErrorComponent />}
resources={[
{
name: "posts",
list: PostList,
show: PostShow,
edit: PostEdit,
create: PostCreate,
},
]}
/>
</ChakraProvider>
);
};

And that's it! Try it on the browser and see if you can create new posts from scratch.

http://localhost:3000/posts/create

Deleting a record

Deleting a record can be done in two ways.

The first way is adding a delete button on each row since refine doesn't automatically add one, so we have to update our <PostList> component to add a <DeleteButton> for each record:

src/pages/posts/list.tsx
import {
...
ShowButton,
EditButton
HStack,
DeleteButton,
} from "@pankod/refine-chakra-ui";

const PostList: React.FC = () => {
const columns = React.useMemo<ColumnDef<IPost>[]>(
() => [
...
{
id: "actions",
header: "Actions",
accessorKey: "id",
enableColumnFilter: false,
enableSorting: false,
cell: function render({ getValue }) {
return (

<HStack>
<ShowButton
hideText
size="sm"
recordItemId={getValue() as number}
/>
<EditButton
hideText
size="sm"
recordItemId={getValue() as number}
/>
<DeleteButton
hideText
size="sm"
recordItemId={getValue() as number}
/>
</HStack>
);
},
},
],
[],
);


const { ... } = useTable<IPost>({ columns });

return (
...
);
};

Refer to the <DeleteButton> documentation for detailed usage information.

Now you can try deleting records yourself. Just click on the delete button of the record you want to delete and confirm.

The second way is showing delete button in <PostEdit> component. To show delete button in edit page, canDelete prop needs to be passed to resource object.

src/App.tsx
import { Refine } from "@pankod/refine-core";
import {
ChakraProvider,
ErrorComponent,
Layout,
refineTheme,
ReadyPage,
notificationProvider,
} from "@pankod/refine-chakra-ui";
import dataProvider from "@pankod/refine-simple-rest";
import routerProvider from "@pankod/refine-react-router-v6";

import { PostList, PostShow, PostEdit, PostCreate } from "./pages";

const App: React.FC = () => {
return (
<ChakraProvider theme={refineTheme}>
<Refine
notificationProvider={notificationProvider()}
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
Layout={Layout}
ReadyPage={ReadyPage}
catchAll={<ErrorComponent />}
resources={[
{
name: "posts",
list: PostList,
show: PostShow,
edit: PostEdit,
create: PostCreate,
canDelete: true,
},
]}
/>
</ChakraProvider>
);
};

After adding canDelete prop, <DeleteButton> will appear in edit form.

http://localhost:3000/posts

Live StackBlitz Example

Our tutorial is complete. Below you'll find a Live StackBlitz Example displaying what we have done so far:

Next Steps