Skip to main content

Refactoring

Now we're ready to refactor our app to use the @refinedev/mui's useTable hook and @refinedev/react-hook-form's useForm hook. These hooks will allow us to create forms and tables with ease by providing an interface that is compatible with Material UI's <Autocomplete /> and <DataGrid /> components.

We'll also be using the field components provided by @refinedev/mui to create product details screen. These components are tailored to work with Material UI's design system and provide an easy way to display various types of data.

Using <DataGrid /> and useDataGrid

We'll start by refactoring our <ListProducts /> component to use the useDataGrid hook from @refinedev/mui and the <DataGrid /> component from Material UI. This will allow us to create a table to display our products with minimal effort.

useDataGrid will give us the same functionality as the core's useTable but will also return the dataGridProps that we can use to pass to the <DataGrid /> component with ease.

Update your src/pages/products/list.tsx file by adding the following lines:

src/pages/products/list.tsx
import React from "react";
import { useMany } from "@refinedev/core";
import { useDataGrid, EditButton, ShowButton } from "@refinedev/mui";

import { DataGrid, GridColDef } from "@mui/x-data-grid";

export const ListProducts = () => {
const { dataGridProps } = useDataGrid<IProduct>({
sorters: { initial: [{ field: "id", order: "asc" }] },
syncWithLocation: true,
});

const { data: categories, isLoading } = useMany<ICategory>({
resource: "categories",
ids: dataGridProps?.rows?.map((product) => product.category?.id) ?? [],
});

// We're defining the columns for our table according to the `<DataGrid />` component's `columns` prop.
const columns = React.useMemo<GridColDef<IProduct>[]>(
() => [
{
field: "id",
headerName: "ID",
type: "number",
width: 50,
},
{
field: "name",
headerName: "Name",
minWidth: 400,
flex: 1,
},
{
field: "category.id",
headerName: "Category",
minWidth: 250,
flex: 0.5,
display: "flex",
renderCell: function render({ row }) {
if (isLoading) return "Loading...";

return categories?.data?.find(
(category) => category.id == row.category.id,
)?.title;
},
},
{
field: "material",
headerName: "Material",
minWidth: 120,
flex: 0.3,
},
{
field: "price",
headerName: "Price",
minWidth: 120,
flex: 0.3,
},
{
field: "actions",
headerName: "Actions",
display: "flex",
renderCell: function render({ row }) {
return (
<div>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
</div>
);
},
},
],
[categories, isLoading],
);

return (
<div>
<h1>Products</h1>
<DataGrid {...dataGridProps} columns={columns} />
</div>
);
};

interface IProduct {
id: number;
name: string;
material: string;
price: string;
category: ICategory;
}

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

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 Custom Filters

Material UI's <DataGrid /> and Refine's useDataGrid hook automatically enables sorters and filters for our resource. Except for the category.id field we've defined. It still works if we provide the id value in filters but let's update our code to provide a single select dropdown for the Category column as filter.

To avoid duplicate requests for the same purpose, we'll switch to useSelect hook instead of useMany hook to fetch the categories. Then, we'll provide the options to the category.id field and tell <DataGrid /> to use a dropdown as the filtering component.

Update your src/pages/products/list.tsx file by adding the following lines:

src/pages/products/list.tsx
import React from "react";
import { useSelect } from "@refinedev/core";
import { useDataGrid, EditButton, ShowButton } from "@refinedev/mui";

import { DataGrid, GridColDef } from "@mui/x-data-grid";

export const ListProducts = () => {
const { dataGridProps } = useDataGrid<IProduct>({
sorters: { initial: [{ field: "id", order: "asc" }] },
syncWithLocation: true,
});

const {
options: categories,
query: { isLoading },
} = useSelect<ICategory>({
resource: "categories",
});

const columns = React.useMemo<GridColDef<IProduct>[]>(
() => [
{
field: "id",
headerName: "ID",
type: "number",
width: 50,
},
{
field: "name",
headerName: "Name",
minWidth: 400,
flex: 1,
},
{
field: "category.id",
headerName: "Category",
minWidth: 250,
flex: 0.5,
// We're defining the column type as `singleSelect` and providing the options to the `valueOptions` prop.
type: "singleSelect",
valueOptions: categories,
display: "flex",
renderCell: function render({ row }) {
if (isLoading) {
return "Loading...";
}

return categories?.find(
(category) => category.value == row.category.id,
)?.label;
},
},
{
field: "material",
headerName: "Material",
minWidth: 120,
flex: 0.3,
},
{
field: "price",
headerName: "Price",
minWidth: 120,
flex: 0.3,
},
{
field: "actions",
headerName: "Actions",
display: "flex",
renderCell: function render({ row }) {
return (
<div>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
</div>
);
},
},
],
[categories, isLoading],
);

return (
<div>
<h1>Products</h1>
<DataGrid {...dataGridProps} columns={columns} />
</div>
);
};

interface IProduct {
id: number;
name: string;
material: string;
price: string;
category: ICategory;
}

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

Now we've added support for both the filters and sorters to our table with minimal effort.

Using useForm and React Hook Form

Next, we'll refactor our <EditProduct /> and <CreateProduct /> components with Material UI elements and the useForm hook from @refinedev/react-hook-form. This will allow us to create forms with minimal effort.

Since Material UI doesn't provide a solution for managing the form state, we'll be using @refinedev/react-hook-form along with the react-hook-form to manage the form state and render our form with Material UI components.

Let's start with installing the required packages:

npm i @refinedev/react-hook-form react-hook-form
Implementation Details

@refinedev/react-hook-form also exports an useForm hook which is an extension of useForm from @refinedev/core. Additionally it provides a state management solution for the forms.

Update your src/pages/products/edit.tsx file by adding the following lines:

src/pages/products/edit.tsx
import { useForm } from "@refinedev/react-hook-form";
import { useAutocomplete, SaveButton } from "@refinedev/mui";

import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";

import { Controller } from "react-hook-form";

export const EditProduct = () => {
const {
register,
control,
saveButtonProps,
refineCore: { query },
formState: { errors },
} = useForm();

const { autocompleteProps } = useAutocomplete({
resource: "categories",
defaultValue: query?.data?.data?.category?.id,
});

return (
<Box
component="form"
sx={{ display: "flex", flexDirection: "column", gap: "12px" }}
>
<TextField
{...register("name")}
label="Name"
error={!!errors.name}
helperText={errors.name?.message}
/>
<TextField
{...register("description")}
multiline
label="Description"
error={!!errors.description}
helperText={errors.description?.message}
/>
<TextField
{...register("material")}
label="Material"
error={!!errors.material}
helperText={errors.material?.message}
/>
{/* We're using Controller to wrap the Autocomplete component and pass the control from useForm */}
<Controller
control={control}
name="category"
defaultValue={null}
render={({ field }) => (
<Autocomplete
id="category"
{...autocompleteProps}
{...field}
onChange={(_, value) => field.onChange(value)}
getOptionLabel={(item) => {
return (
autocompleteProps?.options?.find(
(option) => option?.id == item?.id,
)?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) => {
return value === undefined || option?.id == (value?.id ?? value);
}}
renderInput={(params) => (
<TextField
{...params}
label="Category"
variant="outlined"
margin="normal"
error={!!errors.category}
helperText={errors.category?.message}
/>
)}
/>
)}
/>

<TextField
{...register("price")}
label="Price"
error={!!errors.price}
helperText={errors.price?.message}
/>

{/* SaveButton renders a submit button to submit our form */}
<SaveButton {...saveButtonProps} />
</Box>
);
};

Notice that we've also used the useAutocomplete hook with <Autocomplete /> component to create an autocomplete input for the category field. useAutocomplete is fully compatible with Material UI's <Autocomplete /> 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 } from "@refinedev/react-hook-form";
import { useAutocomplete, SaveButton } from "@refinedev/mui";

import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";

import { Controller } from "react-hook-form";

export const CreateProduct = () => {
const {
register,
control,
saveButtonProps,
formState: { errors },
} = useForm();

const { autocompleteProps } = useAutocomplete({
resource: "categories",
});

return (
<Box
component="form"
sx={{ display: "flex", flexDirection: "column", gap: "12px" }}
>
<TextField
{...register("name")}
label="Name"
error={!!errors.name}
helperText={errors.name?.message}
/>
<TextField
{...register("description")}
multiline
label="Description"
error={!!errors.description}
helperText={errors.description?.message}
/>
<TextField
{...register("material")}
label="Material"
error={!!errors.material}
helperText={errors.material?.message}
/>
{/* We're using Controller to wrap the Autocomplete component and pass the control from useForm */}
<Controller
control={control}
name="category"
defaultValue={null}
render={({ field }) => (
<Autocomplete
id="category"
{...autocompleteProps}
{...field}
onChange={(_, value) => field.onChange(value)}
getOptionLabel={(item) => {
return (
autocompleteProps?.options?.find(
(option) => option?.id == item?.id,
)?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) => {
return value === undefined || option?.id == (value?.id ?? value);
}}
renderInput={(params) => (
<TextField
{...params}
label="Category"
variant="outlined"
margin="normal"
error={!!errors.category}
helperText={errors.category?.message}
/>
)}
/>
)}
/>

<TextField
{...register("price")}
label="Price"
error={!!errors.price}
helperText={errors.price?.message}
/>

{/* SaveButton renders a submit button to submit our form */}
<SaveButton {...saveButtonProps} />
</Box>
);
};

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/mui 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 {
TextFieldComponent as TextField,
NumberField,
MarkdownField,
} from "@refinedev/mui";

import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";

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 (
<Stack gap={1}>
<Typography variant="body1" fontWeight="bold">
Id
</Typography>
<TextField value={data?.data?.id} />

<Typography variant="body1" fontWeight="bold">
Name
</Typography>
<TextField value={data?.data?.name} />

<Typography variant="body1" fontWeight="bold">
Description
</Typography>
<MarkdownField value={data?.data?.description} />

<Typography variant="body1" fontWeight="bold">
Material
</Typography>
<TextField value={data?.data?.material} />

<Typography variant="body1" fontWeight="bold">
Category
</Typography>
<TextField
value={categoryIsLoading ? "Loading..." : categoryData?.data?.title}
/>

<Typography variant="body1" fontWeight="bold">
Price
</Typography>
<NumberField value={data?.data?.price} />
</Stack>
);
};

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/mui, what they are and why they are useful.

Was this helpful?
import { Refine, Authenticated } from "@refinedev/core";
import routerProvider, { NavigateToResource } from "@refinedev/react-router";

import { BrowserRouter, Routes, Route, Outlet } from "react-router";

import { RefineThemes, ThemedLayoutV2, ThemedTitleV2 } from "@refinedev/mui";

import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import { ThemeProvider } from "@mui/material/styles";

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";

export default function App(): JSX.Element {
  return (
    <BrowserRouter>
      <ThemeProvider theme={RefineThemes.Blue}>
        <CssBaseline />
        <GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
        <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>
      </ThemeProvider>
    </BrowserRouter>
  );
}
installing dependencies
installing dependencies