Introduction
In the previous unit, we learned about the router integrations of Refine. Now, we'll dive into its UI integrations, layouts, CRUD view components, and hooks to build a CRUD application with Refine and Material UI.
Refine provides integrations for the popular UI libraries including Material UI, Ant Design, Chakra UI and Mantine, offering set of components and hooks that simplify using Refine for form and table management, layouts, views, buttons, and more.
This unit will cover the following topics:
- Using layout components to add menus, headers, breadcrumbs and authentication management to your app,
- Using CRUD view components to create action pages with consistent design and common features,
- Using hooks to integrate form elements and tables with Refine's
useTable
anduseForm
hooks, - Integrating Refine's notifications with Material UI's notification system using
notistack
, - Using
<AuthPage />
components to easily manage authentication pages.
Material UI comes with form elements but doesn't offer a way to manage form state. In this tutorial, we'll use Refine's React Hook Form integration @refinedev/react-hook-form
to manage form state. This package is an extension on top of Refine's useForm
hook. Installation of this package will be done in the next steps when we're refactoring the form components.
Adding Material UI Dependencies
Let's get started with adding our dependencies. To use Material UI components and access Refine's integrated hooks and components, we need to install the @refinedev/mui
package.
- npm
- pnpm
- yarn
npm i @refinedev/mui @emotion/react @emotion/styled @mui/lab @mui/material @mui/x-data-grid
pnpm add @refinedev/mui @emotion/react @emotion/styled @mui/lab @mui/material @mui/x-data-grid
yarn add @refinedev/mui @emotion/react @emotion/styled @mui/lab @mui/material @mui/x-data-grid
We'll wrap our app with Material UI's <ThemeProvider />
to set the theme and mount <CssBaseline />
and <GlobalStyles />
components to reset the default styles of the browser.
<ThemeProvider />
component requires a theme
prop to be passed. We'll use the predefined themes provided by Refine with many color options and a dark mode.
Refine provides carefully crafted themes for its UI integrations. You can use them to fasten your development process and have a consistent design across your app.
Update your src/App.tsx
file by adding the following lines:
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 } 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";
import { Header } from "./components/header";
export default function App(): JSX.Element {
return (
<BrowserRouter>
{/* We're using Refine's Blue theme here. You can use other variants or create your own theme without constraints. */}
<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"
>
<Header />
<Outlet />
</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>
);
}
With our dependencies now in place, let's proceed by adding a layout into our app.
Adding a Layout
Refine provides a <ThemedLayoutV2 />
component has out of the box features, which we'll delve into in the next step. Now to see it in action, let's wrap our authenticated routes with it.
Update your src/App.tsx
file by adding the following lines:
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 } 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>
<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>
);
}
Now our app is wrapped with a nice layout including a sidebar and a header.
In the next step, we'll learn about the features of the layout components and how to use them.
import { useTable, useMany, useNavigation } from "@refinedev/core"; export const ListProducts = () => { const { tableQuery: { data, isLoading }, current, setCurrent, pageCount, sorters, setSorters, } = useTable({ pagination: { current: 1, pageSize: 10 }, sorters: { initial: [{ field: "id", order: "asc" }] }, syncWithLocation: true, }); const { show, edit } = useNavigation(); const { data: categories } = useMany({ resource: "categories", ids: data?.data?.map((product) => product.category?.id) ?? [], }); if (isLoading) { return <div>Loading...</div>; } const onPrevious = () => { if (current > 1) { setCurrent(current - 1); } }; const onNext = () => { if (current < pageCount) { setCurrent(current + 1); } }; const onPage = (page: number) => { setCurrent(page); }; const getSorter = (field: string) => { const sorter = sorters?.find((sorter) => sorter.field === field); if (sorter) { return sorter.order; } }; const onSort = (field: string) => { const sorter = getSorter(field); setSorters( sorter === "desc" ? [] : [ { field, order: sorter === "asc" ? "desc" : "asc", }, ], ); }; const indicator = { asc: "⬆️", desc: "⬇️" }; return ( <div> <h1>Products</h1> <table> <thead> <tr> <th onClick={() => onSort("id")}> ID {indicator[getSorter("id")]} </th> <th onClick={() => onSort("name")}> Name {indicator[getSorter("name")]} </th> <th>Category</th> <th onClick={() => onSort("material")}> Material {indicator[getSorter("material")]} </th> <th onClick={() => onSort("price")}> Price {indicator[getSorter("price")]} </th> <th>Actions</th> </tr> </thead> <tbody> {data?.data?.map((product) => ( <tr key={product.id}> <td>{product.id}</td> <td>{product.name}</td> <td> { categories?.data?.find( (category) => category.id == product.category?.id, )?.title } </td> <td>{product.material}</td> <td>{product.price}</td> <td> <button type="button" onClick={() => show("protected-products", product.id)} > Show </button> <button type="button" onClick={() => edit("protected-products", product.id)} > Edit </button> </td> </tr> ))} </tbody> </table> <div className="pagination"> <button type="button" onClick={onPrevious}> {"<"} </button> <div> {current - 1 > 0 && ( <span onClick={() => onPage(current - 1)}>{current - 1}</span> )} <span className="current">{current}</span> {current + 1 < pageCount && ( <span onClick={() => onPage(current + 1)}>{current + 1}</span> )} </div> <button type="button" onClick={onNext}> {">"} </button> </div> </div> ); };