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:
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,
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",
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} autoHeight />
</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:
<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 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:
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,
// Since now the options are in an object format, we need to provide the `valueFormatter` prop to pick the value of the option.
valueFormatter: (params) => params.value,
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",
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} autoHeight />
</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
- pnpm
- yarn
npm i @refinedev/react-hook-form react-hook-form
pnpm add @refinedev/react-hook-form react-hook-form
yarn add @refinedev/react-hook-form react-hook-form
@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:
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:
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:
<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.<MarkdownField />
, displays a GitHub flavored markdown withreact-makrdown
library.<NumberField />
, displays a number with localized and customizable formatting.<TagField />
, displays the value with Material UI's<Chip />
component.<TextField />
, displays the value with Material UI's<Typography />
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 {
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.
import { Refine, Authenticated } from "@refinedev/core"; import routerProvider, { NavigateToResource } from "@refinedev/react-router-v6"; import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom"; 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> ); }