Skip to main content

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:

src/pages/products/list.tsx
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:

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:

src/pages/products/list.tsx
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:

src/pages/products/list.tsx
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.

Implementation Details

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:

src/pages/products/edit.tsx
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:

src/pages/products/create.tsx
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:

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:

src/pages/products/show.tsx
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.

Was this helpful?
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>
  );
}
installing dependencies
installing dependencies