Skip to main content

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 and useForm hooks,
  • Integrating Refine's notifications with Material UI's notification system using notistack,
  • Using <AuthPage /> components to easily manage authentication pages.
NOTE

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 i @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.

TIP

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:

src/App.tsx
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:

src/App.tsx
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.

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