Refactoring
Now we're ready to refactor our app to use the @refinedev/antd
's useForm
and useTable
hooks. These hooks will allow us to create forms and tables with ease by providing an interface that is compatible with Ant Design's <Form />
and <Table />
components.
We'll also be using the field components provided by @refinedev/antd
to create product details screen. These components are tailored to work with Ant Design's design system and provide an easy way to display various types of data.
Using <Table />
and useTable
We'll start by refactoring our <ListProducts />
component to use the useTable
hook from @refinedev/antd
and the <Table />
component from Ant Design. This will allow us to create a table to display our products with minimal effort.
useTable
will give us the same functionality as the core version but will also return the tableProps
that we can use to pass to the <Table />
component with ease.
Update your src/pages/products/list.tsx
file by adding the following lines:
import { useMany } from "@refinedev/core";
import { useTable, EditButton, ShowButton } from "@refinedev/antd";
import { Table, Space } from "antd";
export const ListProducts = () => {
// We'll use pass `tableProps` to the `<Table />` component,
// This will manage the data, pagination, filters and sorters for us.
const { tableProps } = useTable({
sorters: { initial: [{ field: "id", order: "asc" }] },
syncWithLocation: true,
});
const { data: categories, isLoading } = useMany({
resource: "categories",
ids: tableProps?.dataSource?.map((product) => product.category?.id) ?? [],
});
return (
<div>
<h1>Products</h1>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="name" title="Name" />
<Table.Column
dataIndex={["category", "id"]}
title="Category"
render={(value) => {
if (isLoading) {
return "Loading...";
}
return categories?.data?.find((category) => category.id == value)
?.title;
}}
/>
<Table.Column dataIndex="material" title="Material" />
<Table.Column dataIndex="price" title="Price" />
<Table.Column
title="Actions"
render={(_, record) => (
<Space>
{/* We'll use the `EditButton` and `ShowButton` to manage navigation easily */}
<ShowButton hideText size="small" recordItemId={record.id} />
<EditButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</div>
);
};
Notice that we've get rid of every logic related to managing the data, pagination, filters and sorters because these will be managed by the tableProps
value. We've also used the EditButton
and ShowButton
components to manage navigation easily.
Button components provided by Refine uses the styling from Ant Design and provides many features from built-in access control to i18n and more.
List of available button components:
<CreateButton />
, renders a button to navigate to the create route.<EditButton />
, renders a button to navigate to the edit route.<ListButton />
, renders a button to navigate to the list route.<ShowButton />
, renders a button to navigate to the show route.<CloneButton />
, renders a button to navigate to the clone route.<DeleteButton />
, renders a button to delete a record.<SaveButton />
, renders a button to trigger the form submission.<RefreshButton />
, renders a button to refresh/refetch the data.<ImportButton />
, renders a button to trigger import bulk data with CSV/Excel files.<ExportButton />
, renders a button to trigger export bulk data with CSV format.
Adding Sorters
Let's integrate the Refine's sorters with the Ant Design's <Table />
component. While tableProps
will be transforming the sorters to the Ant Design's format, all we need to do is to enable the sorters for the columns we want and pass the default sorting orders.
We'll add sorting to the ID
and the Name
columns.
Update your src/pages/products/list.tsx
file by adding the following lines:
import { useMany } from "@refinedev/core";
import {
useTable,
EditButton,
ShowButton,
getDefaultSortOrder,
} from "@refinedev/antd";
import { Table, Space } from "antd";
export const ListProducts = () => {
const { tableProps, sorters } = useTable({
sorters: { initial: [{ field: "id", order: "asc" }] },
syncWithLocation: true,
});
const { data: categories, isLoading } = useMany({
resource: "categories",
ids: tableProps?.dataSource?.map((product) => product.category?.id) ?? [],
});
return (
<div>
<h1>Products</h1>
<Table {...tableProps} rowKey="id">
<Table.Column
dataIndex="id"
title="ID"
sorter
defaultSortOrder={getDefaultSortOrder("id", sorters)}
/>
<Table.Column
dataIndex="name"
title="Name"
sorter
defaultSortOrder={getDefaultSortOrder("name", sorters)}
/>
<Table.Column
dataIndex={["category", "id"]}
title="Category"
render={(value) => {
if (isLoading) {
return "Loading...";
}
return categories?.data?.find((category) => category.id == value)
?.title;
}}
/>
<Table.Column dataIndex="material" title="Material" />
<Table.Column dataIndex="price" title="Price" />
<Table.Column
title="Actions"
render={(_, record) => (
<Space>
<ShowButton hideText size="small" recordItemId={record.id} />
<EditButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</div>
);
};
Now we've enabled sorters with no additional logic. The getDefaultSortOrder
function will handle the default sorting orders for us.
Adding Filters
Let's integrate the Refine's filters with the Ant Design's <Table />
component. While tableProps
will be transforming the filters to the Ant Design's format, all we need to do is to provide the elements for the filter dropdowns. We'll use the <FilterDropdown />
to bind the inputs to the filters.
We'll be using the <Input />
component to create a text filter for the Name
column and the <Select />
component with useSelect
to create a select filter for the Category
column.
Update your src/pages/products/list.tsx
file by adding the following lines:
import { useMany, getDefaultFilter } from "@refinedev/core";
import {
useTable,
EditButton,
ShowButton,
getDefaultSortOrder,
FilterDropdown,
useSelect,
} from "@refinedev/antd";
import { Table, Space, Input, Select } from "antd";
export const ListProducts = () => {
const { tableProps, sorters, filters } = useTable({
sorters: { initial: [{ field: "id", order: "asc" }] },
// We're adding default values for our filters
filters: {
initial: [{ field: "category.id", operator: "eq", value: 2 }],
},
syncWithLocation: true,
});
const { data: categories, isLoading } = useMany({
resource: "categories",
ids: tableProps?.dataSource?.map((product) => product.category?.id) ?? [],
});
const { selectProps } = useSelect({
resource: "categories",
defaultValue: getDefaultFilter("category.id", filters, "eq"),
});
return (
<div>
<h1>Products</h1>
<Table {...tableProps} rowKey="id">
<Table.Column
dataIndex="id"
title="ID"
sorter
defaultSortOrder={getDefaultSortOrder("id", sorters)}
/>
<Table.Column
dataIndex="name"
title="Name"
sorter
defaultSortOrder={getDefaultSortOrder("name", sorters)}
// FilterDropdown will map the value to the filter
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Input />
</FilterDropdown>
)}
/>
<Table.Column
dataIndex={["category", "id"]}
title="Category"
render={(value) => {
if (isLoading) {
return "Loading...";
}
return categories?.data?.find((category) => category.id == value)
?.title;
}}
filterDropdown={(props) => (
<FilterDropdown
{...props}
// We'll store the selected id as number
mapValue={(selectedKey) => Number(selectedKey)}
>
<Select style={{ minWidth: 200 }} {...selectProps} />
</FilterDropdown>
)}
defaultFilteredValue={getDefaultFilter("category.id", filters, "eq")}
/>
<Table.Column dataIndex="material" title="Material" />
<Table.Column dataIndex="price" title="Price" />
<Table.Column
title="Actions"
render={(_, record) => (
<Space>
<ShowButton hideText size="small" recordItemId={record.id} />
<EditButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</div>
);
};
Now we've added both the filters and sorters to our table without writing any additional logic.
Remember that we've only implemented the eq
filter in our data provider. Even though category.id
field is best to be filtered with in
operator and name
field is best to be filtered with contains
operators. We're keeping it simple for the sake of this tutorial.
Using useForm
and <Form />
Next, we'll refactor our <EditProduct />
and <CreateProduct />
components to use the useForm
hook from @refinedev/antd
and the <Form />
component from Ant Design. This will allow us to create forms with minimal effort.
useForm
will give us the same functionality as the core version but will also return the formProps
that we can use to pass to the <Form />
component with ease.
Update your src/pages/products/edit.tsx
file by adding the following lines:
import { useForm, useSelect, SaveButton } from "@refinedev/antd";
import { Form, Input, Select, InputNumber } from "antd";
export const EditProduct = () => {
const { formProps, saveButtonProps, query } = useForm({
refineCoreProps: {
redirect: "show",
},
});
const { selectProps } = useSelect({
resource: "categories",
defaultValue: query?.data?.data?.category?.id,
});
return (
<Form {...formProps} layout="vertical">
<Form.Item label="Name" name="name">
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<Input.TextArea />
</Form.Item>
<Form.Item label="Material" name="material">
<Input />
</Form.Item>
<Form.Item label="Category" name={["category", "id"]}>
<Select {...selectProps} />
</Form.Item>
<Form.Item label="Price" name="price">
<InputNumber step="0.01" stringMode />
</Form.Item>
{/* SaveButton renders a submit button to submit our form */}
<SaveButton {...saveButtonProps} />
</Form>
);
};
Notice that we've also used the useSelect
hook with <Select />
component to create a select input for the category
field. useSelect
is fully compatible with Ant Design's <Select />
component and provides an easy way to create select inputs with minimal effort.
Now let's do the same for the CreateProduct
component. These components will use mostly the same logic except the edit action will provide default values for the fields.
Update your src/pages/products/create.tsx
file by adding the following lines:
import { useForm, useSelect, SaveButton } from "@refinedev/antd";
import { Form, Input, Select, InputNumber } from "antd";
export const CreateProduct = () => {
const { formProps, saveButtonProps } = useForm({
refineCoreProps: {
redirect: "edit",
},
});
const { selectProps } = useSelect({
resource: "categories",
});
return (
<Form {...formProps} layout="vertical">
<Form.Item label="Name" name="name">
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<Input.TextArea />
</Form.Item>
<Form.Item label="Material" name="material">
<Input />
</Form.Item>
<Form.Item label="Category" name={["category", "id"]}>
<Select {...selectProps} />
</Form.Item>
<Form.Item label="Price" name="price">
<InputNumber step="0.01" stringMode />
</Form.Item>
<SaveButton {...saveButtonProps} />
</Form>
);
};
Refactoring <ShowProduct />
Now that we've refactored our list, edit and create components, let's refactor our <ShowProduct />
component to use the field components from @refinedev/antd
to represent every field with proper styling and formatting.
List of available field components:
<BooleanField />
, displays a checkbox element for boolean values.<DateField />
, displays a date with customizable formatting.<EmailField />
, displays an email with a mailto anchor.<FileField />
, displays a download anchor for file.<ImageField />
, displays an image with Ant Design's<Image />
component.<MarkdownField />
, displays a GitHub flavored markdown withreact-makrdown
library.<NumberField />
, displays a number with localized and customizable formatting.<TagField />
, displays the value with Ant Design's<Tag />
component.<TextField />
, displays the value with Ant Design's<Typography.Text />
component.<UrlField />
, displays the value with a link anchor.
We'll be using the <TextField />
, <NumberField />
and <MarkdownField />
components to represent the fields of the products properly.
Update your src/pages/products/show.tsx
file by adding the following lines:
import { useShow, useOne } from "@refinedev/core";
import { TextField, NumberField, MarkdownField } from "@refinedev/antd";
import { Typography } from "antd";
export const ShowProduct = () => {
const {
query: { data, isLoading },
} = useShow();
const { data: categoryData, isLoading: categoryIsLoading } = useOne({
resource: "categories",
id: data?.data?.category.id || "",
queryOptions: {
enabled: !!data?.data,
},
});
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<Typography.Title level={5}>Id</Typography.Title>
<TextField value={data?.data?.id} />
<Typography.Title level={5}>Name</Typography.Title>
<TextField value={data?.data?.name} />
<Typography.Title level={5}>Description</Typography.Title>
<MarkdownField value={data?.data?.description} />
<Typography.Title level={5}>Material</Typography.Title>
<TextField value={data?.data?.material} />
<Typography.Title level={5}>Category</Typography.Title>
<TextField
value={categoryIsLoading ? "Loading..." : categoryData?.data?.title}
/>
<Typography.Title level={5}>Price</Typography.Title>
<NumberField value={data?.data?.price} />
</div>
);
};
Now we've updated all our routes to use extended versions of Refine's core hooks and helper components.
In the next step, we'll be learning about the CRUD views provided by @refinedev/antd
, what they are and why they are useful.
import { Refine, Authenticated } from "@refinedev/core"; import routerProvider, { NavigateToResource } from "@refinedev/react-router-v6"; import { ThemedLayoutV2, ThemedTitleV2 } from "@refinedev/antd"; import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom"; import { ConfigProvider, App as AntdApp } from "antd"; import { dataProvider } from "./providers/data-provider"; import { authProvider } from "./providers/auth-provider"; import { ShowProduct } from "./pages/products/show"; import { EditProduct } from "./pages/products/edit"; import { ListProducts } from "./pages/products/list"; import { CreateProduct } from "./pages/products/create"; import { Login } from "./pages/login"; import "antd/dist/reset.css"; export default function App(): JSX.Element { return ( <BrowserRouter> <ConfigProvider> <AntdApp> <Refine dataProvider={dataProvider} authProvider={authProvider} routerProvider={routerProvider} resources={[ { name: "protected-products", list: "/products", show: "/products/:id", edit: "/products/:id/edit", create: "/products/create", meta: { label: "Products" }, }, ]} > <Routes> <Route element={ <Authenticated key="authenticated-routes" redirectOnFail="/login" > <ThemedLayoutV2 Title={(props) => ( <ThemedTitleV2 {...props} text="Awesome Project" /> )} > <Outlet /> </ThemedLayoutV2> </Authenticated> } > <Route index element={<NavigateToResource resource="protected-products" />} /> <Route path="/products"> <Route index element={<ListProducts />} /> <Route path=":id" element={<ShowProduct />} /> <Route path=":id/edit" element={<EditProduct />} /> <Route path="create" element={<CreateProduct />} /> </Route> </Route> <Route element={ <Authenticated key="auth-pages" fallback={<Outlet />}> <NavigateToResource resource="protected-products" /> </Authenticated> } > <Route path="/login" element={<Login />} /> </Route> </Routes> </Refine> </AntdApp> </ConfigProvider> </BrowserRouter> ); }