Using Refine to Build an Admin Panel
Refine is a React Framework for building internal tools, admin panels, dashboards & B2B apps.
Introduction
Admin panels are data-intensive business-to-business (B2B) applications that handle complex business logic. They integrate with various data sources, systems, and platforms, making them the hallmark of business management on the web.
In this article, we will look at how to use Refine and Material UI to build a minimal but complete React admin panel. By the end of this tutorial, you will have a fully functional dashboard that displays key performance indicators, charts, and tables for your business and pages with CRUD operations.
⭐ You can get the source code for the sample app we’re building in this tutorial here.
What is Refine?
Refine is an Open-source React meta-framework for building internal tools and enterprise applications, such as admin panels, dashboards, and other B2B applications. It is a feature-rich framework with built-in support for routing, authentication, internationalization, state management, and more.
Refine offers collections of helper hooks, components, providers, and more rather than limiting you to pre-styled components. With its decoupled business logic and UI, you can freely customize the UI. This means refine integrates seamlessly with any custom designs or UI frameworks.
Its headless architecture allows compatibility with popular CSS frameworks like TailwindCSS or even lets you craft your styles from the ground up. Additionally, it provides integrations with Ant Design, Material UI, Mantine, and Chakra UI.
Set up a Refine app
You can create a Refine application using the CLI or the Browser-based Scaffolder.
This tool enables seamless app creation right in your browser. It allows you to customize your application according to your preferences by selecting the libraries and frameworks you wish to use, and the tool will then generate a starter project for you.
For this tutorial, we’ll select the following options:
React Platform: Vite
UI Framework: Material UI
Backend: REST API
Authentication Provider: No Auth
Once you’ve completed the steps, you’ll have the ability to download your project.
After creating the project, you can download it to your local machine. You should sign into the platform with your GitHub or Google account to build and download the project.
Then, use the command below to install dependencies.
npm install
After installing dependencies, use the command below to launch the development server on localhost. You can then view the project in a web browser.
npm run dev
Why Material UI
Material UI is a flexible library comprising a wide range of pre-styled components, streamlining the process of building web applications while adhering to the best practices of user interface design.
Refine offers built-in support for Material UI. This means we do not have to go through the tedious process of configuring the library from scratch, as refine provides all the necessary tools and resources to start with Material UI in a Refine project.
Cleanup and structuring
When scaffolding a new Refine application, tool generates pre-configured pages based on the backend architecture and API the project is bootstrapped with. In this case, the project has been bootstrapped with a REST API backend with a blog endpoint. Thus, the project contains blog-post pages, which are unnecessary for this tutorial and should be removed.
To do this, navigate to the src/pages
directory and delete the blog-posts
and categories
folders.
Next, update the src/App.tsx
file by deleting the objects for the blog-posts
and categories
resources and replacing the endpoint on the data provider with a fake food store API, which the refine team has put together for learning purposes.
However, for the sake of simplicity, you may copy and paste the code below into the src/App.tsx
file instead.
import { GitHubBanner, Refine } from "@refinedev/core";
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import {
ErrorComponent,
notificationProvider,
RefineSnackbarProvider,
ThemedLayoutV2,
ThemedTitleV2,
} from "@refinedev/mui";
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import routerBindings, {
DocumentTitleHandler,
NavigateToResource,
UnsavedChangesNotifier,
} from "@refinedev/react-router-v6";
import dataProvider from "@refinedev/simple-rest";
import { useTranslation } from "react-i18next";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import { AppIcon } from "./components/app-icon";
import { Header } from "./components/header";
import { ColorModeContextProvider } from "./contexts/color-mode";
function App() {
const { t, i18n } = useTranslation();
const i18nProvider = {
translate: (key: string, params: object) => t(key, params),
changeLocale: (lang: string) => i18n.changeLanguage(lang),
getLocale: () => i18n.language };
return (
<BrowserRouter>
<GitHubBanner />
<RefineKbarProvider>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<RefineSnackbarProvider>
<DevtoolsProvider>
<Refine
dataProvider={dataProvider("https://api.finefoods.refine.dev")}
notificationProvider={notificationProvider}
i18nProvider={i18nProvider}
routerProvider={routerBindings}
resources={[
{
name: "dashboard",
list: "/dashboard",
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
projectId: "RjAC3m-NeoqYc-bB545U",
}}
>
<Routes>
<Route
element={
<ThemedLayoutV2
Header={() => <Header sticky />}
Title={({ collapsed }) => (
<ThemedTitleV2
collapsed={collapsed}
text="Fine-food Dashboard"
icon={<AppIcon />}
/>
)}
>
<Outlet />
</ThemedLayoutV2>
}
>
<Route
index
element={<NavigateToResource resource="dashboard" />}
/>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
<DevtoolsPanel />
</DevtoolsProvider>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</RefineKbarProvider>
</BrowserRouter>
);
}
export default App;
Install rechart and other packages
Material UI doesn’t have a dedicated component for rendering charts. Therefore, before proceeding with the tutorial, we must install two additional packages: Rechart and DayJS.
The Rechart library will help us render lightweight charts and graphs in our dashboard, while the DayJS library will assist in querying data from the API using specified dates and times.
To install these packages, run the following commands in your terminal:
npm install recharts dayjs
Building the dashboard
Now that the project has been set up and the necessary dependencies have been installed, we can build the dashboard.
The dashboard application we are developing will track sales records for a restaurant business. It will have three pages: the dashboard
, categories
, and products
pages.
The dashboard page will contain key performance indicator (KPI) cards, charts, and a recent orders table to provide a quick business overview.
These pages also permit users to create, edit, and view products and categories.
The dashboard of the application will be as follows:
Interfaces
To avoid the inconvenience of creating interfaces and types for our data as we proceed, we will declare the interfaces and types we will use throughout the application in the src/interfaces/index.ts
directory beforehand.
Create the directory and add the code below.
export type KpiCardProps = {
title: string;
total: number;
trend: number;
target: number;
formatTotal?: (value: number) => number | string;
formatTarget?: (value: number) => number | string;
};
export type DeltaType =
| "error"
| "warning"
| "primary"
| "secondary"
| "success"
| "info";
export interface IOrder {
id: number;
user: IUser;
createdAt: string;
status: IOrderStatus;
address: IAddress;
amount: number;
}
export interface IOrderStatus {
id: number;
text: "Pending" | "Ready" | "On The Way" | "Delivered" | "Cancelled";
}
export interface IChartDatum {
date: string;
value: string;
}
export interface IChart {
data: IChartDatum[];
total: number;
trend: number;
}
export type IUser = {
id: number;
name: string;
avatar: string;
};
interface IAreaGraphProps {
data: IChartDatum[];
stroke: string;
fill: string;
}
interface IBarChartProps {
data: IChartDatum[];
fill: string;
}
Create a Dashboard page
To create the dashboard page, navigate to the src/pages/dahsboard/dashboard.tsx
file and add the code below:
import React from "react";
function Dashboard() {
return <h1>Hello World</h1>;
}
export default Dashboard;
Next, return to the src/App.tsx
file and import the newly created dashboard page. Add it to the resources
prop on the <Refine/>
component, and add a dashboard
route to the routes
component, as seen in the codes below.
import Dashboard from "./pages/dashboard/dashboard";
...
<Refine
dataProvider={dataProvider("https://api.finefoods.refine.dev")}
notificationProvider={notificationProvider}
i18nProvider={i18nProvider}
routerProvider={routerBindings}
resources={[
{
name: "dashboard",
list: "/dashboard",
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
projectId: "RjAC3m-NeoqYc-bB545U",
}}
>
<Routes>
...
<Route index element={<NavigateToResource resource="dashboard" />} />
<Route path="/dashboard">
<Route index element={<Dashboard />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Routes>
...
</Refine>;
If you go back to the browser, you should see a similar page as the one below.
The dashboard page for our application is now ready for use. We can add the key performance indicators (KPIs) to the page.
Add KPI cards
The KPI cards are the first components we will add to the dashboard. In this section, we will create a base for the card inside the src/component
directory, then import it inside the dashboard
page and reuse the component for all three cards. Each card will represent the weekly revenue, orders, and new customers.
As a first step, navigate to the src/component
directory and create a new folder with a namekpi-card
, and then add the following code.
import React from "react";
import { Card, Box, CardContent, Typography, Chip } from "@mui/material";
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import LinearProgress, {
LinearProgressProps,
} from "@mui/material/LinearProgress";
import { KpiCardProps, DeltaType } from "../../interfaces";
const getColor = (num: number): DeltaType => {
switch (true) {
case num < 20:
return "error";
case num < 50:
return "warning";
case num === 50:
return "info";
case num < 75:
return "primary";
case num < 90:
return "secondary";
default:
return "success";
}
};
function LinearProgressWithLabel(
props: LinearProgressProps & { value: number },
) {
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ width: "100%", mr: 1 }}>
<LinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.value,
)}%`}</Typography>
</Box>
</Box>
);
}
export default function KpiCard({
title,
total,
trend,
target,
formatTotal = (value) => value,
formatTarget = (value) => value,
}: KpiCardProps) {
const percent = Math.round((trend / total) * 100);
const color = getColor(percent);
const arbitraryTarget = Math.round((total / target) * 100);
return (
<Card elevation={3}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
listStyle: "none",
p: 0.5,
m: 0,
}}
>
<CardContent>
<Typography variant="h5" sx={{ fontSize: 17, mb: 0 }}>
{title}
</Typography>
<Typography
variant="h5"
sx={{ fontSize: 40, fontWeight: 700, mb: 0 }}
color={color == "success" ? "green" : color}
>
{formatTotal(total)}
</Typography>
</CardContent>
<CardContent>
<Chip
icon={percent < 0 ? <ArrowDownwardIcon /> : <ArrowUpwardIcon />}
color={color}
label={`${percent}%`}
/>
</CardContent>
</Box>
<Box sx={{ flexGrow: 1 }}>
<CardContent>
<span>{`(Target: ${formatTarget(target)})`}</span>
<LinearProgressWithLabel
value={arbitraryTarget}
variant="determinate"
color={color}
sx={{
borderRadius: 5,
height: 10,
backgroundColor: (theme) =>
theme.palette.mode === "light" ? color : color,
}}
/>
</CardContent>
</Box>
</Card>
);
}
The code above uses the MUIs Card
, Typography
, Chip
, and LinearProgressWithLabel
components to construct a card with the expected data props.
To proceed, return to the src/pages/dashboard/Dashboard.tsx
file and update it with the following code:
import React, { useState } from "react";
import { useApiUrl, useCustom } from "@refinedev/core";
import dayjs from "dayjs";
import { Box, Grid, Tab, Card, CardHeader, styled } from "@mui/material";
import KpiCard from "../../components/kpi-card";
import { IChart } from "../../interfaces";
const query = {
start: dayjs().subtract(7, "days").startOf("day"),
end: dayjs().startOf("day"),
};
const formatCurrency = Intl.NumberFormat("en", {
style: "currency",
currency: "USD",
});
export function Dashboard() {
const API_URL = useApiUrl();
const { data: revenue } = useCustom<IChart>({
url: `${API_URL}/dailyRevenue`,
method: "get",
config: {
query,
},
});
const { data: orders } = useCustom<IChart>({
url: `${API_URL}/dailyOrders`,
method: "get",
config: {
query,
},
});
const { data: customers } = useCustom<IChart>({
url: `${API_URL}/newCustomers`,
method: "get",
config: {
query,
},
});
return (
<main>
<Box mt={2} mb={5}>
<Grid container columnGap={3} rowGap={3}>
<Grid item xs>
<KpiCard
title="Weekly Revenue"
total={revenue?.data.total ?? 0}
trend={revenue?.data.trend ?? 0}
target={10000}
formatTotal={(value) => formatCurrency.format(value)}
formatTarget={(value) => formatCurrency.format(value)}
/>
</Grid>
<Grid item xs>
<KpiCard
title="Weekly Orders"
total={orders?.data.total ?? 0}
trend={orders?.data.trend ?? 0}
target={150}
/>
</Grid>
<Grid item xs>
<KpiCard
title="New Customers"
total={customers?.data.total ?? 0}
trend={customers?.data.trend ?? 0}
target={300}
/>
</Grid>
</Grid>
</Box>
</main>
);
}
Here, we use the useCustom
hook from refine to query data from the dailyRevenue
, dailyOrders
, and newCustomers
endpoints of the fine-food API. It's used to send custom query requests using the Tanstack Query advantages. The base URL for the API is made available using the useApiUrl
hook.
We also use the dayJS
package that we installed previously to help the useCustom
component query data for the last week from the API.
After saving the code, the cards should display on the browser, as shown in the following image.
Add KPI charts using Rechart
To create the chart section of our dashboard, we will add two types of charts that will represent the weekly revenue, orders, and new customers.
Navigate to create a charts
folder, then add AreaChart
and BarChart
subfolders. In AreaChart
, create an index.tsx
file and input the given code.
import React, { useContext } from "react";
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { IAreaGraphProps } from "../../interfaces";
export const formatDate = new Intl.DateTimeFormat("en-US", {
month: "short",
year: "numeric",
day: "numeric",
});
export const AreaGraph: React.FC<IAreaGraphProps> = ({
data,
stroke,
fill,
}) => {
const transformedData = data.map(({ date, value }) => ({
date: formatDate.format(new Date(date)),
value,
}));
return (
<ResponsiveContainer width="99%" aspect={3}>
<AreaChart data={transformedData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis dataKey="value" />
<Tooltip label="Daily Revenue" />
<Area type="monotone" dataKey="value" stroke={stroke} fill={fill} />
</AreaChart>
</ResponsiveContainer>
);
};
Do the same for the BarChart
folder: add an index.tsx
file with the following code:
import React from "react";
import {
BarChart as Chart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { IBarChartProps } from "../../interfaces";
const formatDate = new Intl.DateTimeFormat("en-US", {
month: "short",
year: "numeric",
day: "numeric",
});
export const BarChart: React.FC<IBarChartProps> = ({ data, fill }) => {
const transformedData = data.map(({ date, value }) => ({
date: formatDate.format(new Date(date)),
value,
}));
return (
<ResponsiveContainer width="99%" aspect={3}>
<Chart data={transformedData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis dataKey="value" />
<Tooltip />
<Bar dataKey="value" fill={fill} />
</Chart>
</ResponsiveContainer>
);
};
Next, navigate back to the root of the charts
folder and add an index.ts
file. We will use this file to export the AreaChart
and Barchart
components.
export { AreaGraph } from "./AreaGraph";
export { BarChart } from "./BarChart";
Finally, go to the Dashboard.tsx
file, import both chart components, and update it with the highlighted code below:
import React, { useState } from "react";
import { useApiUrl, useCustom } from "@refinedev/core";
import dayjs from "dayjs";
import { Box, Grid, Tab, Card, CardHeader, styled } from "@mui/material";
import { TabContext } from "@mui/lab";
import TabList from "@mui/lab/TabList";
import TabPanel from "@mui/lab/TabPanel";
import KpiCard from "../../components/kpi-card";
import { AreaGraph, BarChart } from "../../components/charts";
import { IChart } from "../../interfaces";
export function Dashboard() {
const [value, setValue] = React.useState("1");
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
setValue(newValue);
};
...
return (
<main>
...
<Box my={5} sx={{ boxShadow: 5 }}>
<Card>
<CardHeader title="Sales Chart" />
<TabContext value={value}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabList
onChange={handleChange}
aria-label="lab API tabs example"
>
<Tab label="Revenues" value="1" />
<Tab label="Orders" value="2" />
<Tab label="Customers" value="3" />
</TabList>
</Box>
<TabPanel value="1">
<AreaGraph
data={revenue?.data?.data ?? []}
stroke="#8884d8"
fill="#cfeafc"
/>
</TabPanel>
<TabPanel value="2">
<BarChart data={orders?.data?.data ?? []} fill="#ffce90" />
</TabPanel>
<TabPanel value="3">
<AreaGraph
data={customers?.data?.data ?? []}
stroke="#00bd56"
fill="#ccf3f3"
/>
</TabPanel>
</TabContext>
</Card>
</Box>
</main>
);
}
The code above is simpler than it may appear. We are simply using the MUI TabContext
component and its peer components to render the charts in three separate tabs: Revenue
, Orders
, and Customers
. We then pass the appropriate data to each chart component declaration.
After making all these changes, your dashboard should resemble the GIF below:
We are nearly finished constructing our dashboard. The only thing left to do is create the `recentSales` table.
Add a recent sales table
The recent sales table will display recent sales with the order ID
, amount
, ordered by
, status
, and more. Users will also be able to search for orders in the table and filter them in ascending or descending order.
Still in the src/components
directory, create a new folder named recent-sales
import React from "react";
import InputAdornment from "@mui/material/InputAdornment";
import SearchIcon from "@mui/icons-material/Search";
import { DataGrid, GridColTypeDef } from "@mui/x-data-grid";
import { useDataGrid } from "@refinedev/mui";
import { Card, CardHeader, Chip, TextField, Stack } from "@mui/material";
import { getDefaultFilter } from "@refinedev/core";
import { IOrder, IOrderStatus } from "../../interfaces";
export function RecentSales() {
const { dataGridProps, setCurrent, setFilters, filters } =
useDataGrid<IOrder>({
resource: "orders",
initialPageSize: 5,
});
const currencyFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
const formatDate = (value: string) => {
return new Date(value).toLocaleString("en-US", {
month: "short",
year: "numeric",
day: "numeric",
});
};
const getColor = (status: IOrderStatus["text"]) => {
switch (status) {
case "Cancelled":
return "error";
case "Ready":
return "success";
case "On The Way":
return "info";
case "Pending":
return "warning";
case "Delivered":
return "secondary";
default:
return "primary";
}
};
const columns = React.useMemo<GridColTypeDef<any>[]>(
() => [
{
field: "id",
headerName: "id",
width: 70,
},
{
field: "user.fullName",
headerName: "Ordered By",
width: 200,
renderCell: ({ row }) => <>{row\["user"\]["fullName"]}</>,
},
{
field: "amount",
headerName: "Amount",
type: "singleSelect",
width: 150,
valueFormatter: ({ value }) => currencyFormatter.format(value),
},
{
field: "user.gender",
headerName: "Gender",
width: 120,
renderCell: ({ row }) => <>{row\["user"\]["gender"]}</>,
},
{
field: "user.gsm",
headerName: "Tel",
width: 170,
renderCell: ({ row }) => <>{row\["user"\]["gsm"]}</>,
},
{
field: "status.text",
headerName: "Status",
width: 160,
type: "singleSelect",
valueOptions: [
"Cancelled",
"Ready",
"On The Way",
"Pending",
"Delivered",
],
renderCell: ({ row }) => (
<Chip
label={row\["status"\]["text"]}
color={getColor(row\["status"\]["text"])}
variant="outlined"
/>
),
},
{
field: "adress.text",
headerName: "Address",
width: 350,
headerAlign: "left",
renderCell: ({ row }) => <>{row\["adress"\]["text"]}</>,
},
{
field: "createdAt",
headerName: "Created At",
width: 200,
renderCell: ({ row }) => <>{formatDate(row["createdAt"])}</>,
},
],
[]
);
return (
<Card elevation={5}>
<Stack
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="center"
paddingRight={2}
>
<CardHeader title="Recent Sales" />
<TextField
value={getDefaultFilter("q", filters, "contains")}
id="outlined-basic"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.value.trim()) {
setCurrent(1);
setFilters([], "replace");
return;
}
setCurrent(1);
setFilters([
{
field: "q",
value: e.target.value,
operator: "contains",
},
]);
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
size="small"
placeholder="Keyword Search"
/>
</Stack>
<DataGrid
{...dataGridProps}
columns={columns as any}
sx={{ pl: 3 }}
autoHeight
pageSizeOptions={[5, 10, 25, 50]}
/>
</Card>
);
}
In this code, we used the DataGrid
component from MUI to render the table and Refine’s useDataGrid
hook for querying data from the /orders
endpoint, filtering, sorting, and pagination. We also used the columns
array to define the table’s structure.
To complete this step, navigate to the pages/dashboard/Dashboard.tsx
file, import the recent-sales
component, and render it below the charts like this.
import React, { useState } from "react";
import { useApiUrl, useCustom } from "@refinedev/core";
import dayjs from "dayjs";
import { Box, Grid, Tab, Card, CardHeader, styled } from "@mui/material";
import { TabContext } from "@mui/lab";
import TabList from "@mui/lab/TabList";
import TabPanel from "@mui/lab/TabPanel";
import KpiCard from "../../components/kpi-card";
import { AreaGraph, BarChart } from "../../components/charts";
import { RecentSales } from "../../components/recent-sales";
import { IChart } from "../../interfaces";
const Responsive = styled("div")(({ theme }) => ({
[theme.breakpoints.down("md")]: {
width: "880px",
},
[theme.breakpoints.down("sm")]: {
width: "374px",
},
}));
// KPI card code
const query = {
start: dayjs().subtract(7, "days").startOf("day"),
end: dayjs().startOf("day"),
};
const formatCurrency = Intl.NumberFormat("en", {
style: "currency",
currency: "USD",
});
export function Dashboard() {
const API_URL = useApiUrl();
const { data: revenue } = useCustom<IChart>({
url: `${API_URL}/dailyRevenue`,
method: "get",
config: {
query,
},
});
const { data: orders } = useCustom<IChart>({
url: `${API_URL}/dailyOrders`,
method: "get",
config: {
query,
},
});
const { data: customers } = useCustom<IChart>({
url: `${API_URL}/newCustomers`,
method: "get",
config: {
query,
},
});
// Chart code
const [value, setValue] = React.useState("1");
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
setValue(newValue);
};
return (
<main>
<Box mt={2} mb={5}>
<Grid container columnGap={3} rowGap={3}>
<Grid item xs>
<KpiCard
title="Weekly Revenue"
total={revenue?.data.total ?? 0}
trend={revenue?.data.trend ?? 0}
target={10000}
formatTotal={(value) => formatCurrency.format(value)}
formatTarget={(value) => formatCurrency.format(value)}
/>
</Grid>
<Grid item xs>
<KpiCard
title="Weekly Orders"
total={orders?.data.total ?? 0}
trend={orders?.data.trend ?? 0}
target={150}
/>
</Grid>
<Grid item xs>
<KpiCard
title="New Customers"
total={customers?.data.total ?? 0}
trend={customers?.data.trend ?? 0}
target={300}
/>
</Grid>
</Grid>
</Box>
<Box my={5} sx={{ boxShadow: 5 }}>
<Card>
<CardHeader title="Sales Chart" />
<TabContext value={value}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabList
onChange={handleChange}
aria-label="lab API tabs example"
>
<Tab label="Revenues" value="1" />
<Tab label="Orders" value="2" />
<Tab label="Customers" value="3" />
</TabList>
</Box>
<TabPanel value="1">
<AreaGraph
data={revenue?.data?.data ?? []}
stroke="#8884d8"
fill="#cfeafc"
/>
</TabPanel>
<TabPanel value="2">
<BarChart data={orders?.data?.data ?? []} fill="#ffce90" />
</TabPanel>
<TabPanel value="3">
<AreaGraph
data={customers?.data?.data ?? []}
stroke="#00bd56"
fill="#ccf3f3"
/>
</TabPanel>
</TabContext>
</Card>
</Box>
<Responsive>
<RecentSales />
</Responsive>
</main>
);
}
The recent sales table concludes the dashboard page’s construction. Your dashboard should look like the GIF below when you save the changes and return to the browser.
Adding the CRUD pages
As previously discussed, we will create CRUD pages for the categories
and products
resources that will allow users to list, create, update, and view products and categories from the fine-food endpoint. We will begin with the products
resources.
Products pages
We want to create four pages for the products
resource: list
, create
, edit
, and show
. These pages will be located in the src/pages/products
directory.
List Page
The product list page will have features and structure similar to the recent-sales
component we created previously. However, the column structure and layout will be different.
The useDataGrid hook provides properties that seamlessly integrate with the MUI X `<DataGrid>` component, offering built-in functionalities like sorting, filtering, and pagination.
Let’s start by creating a list.tsx
file in the src/page/products
directory we created earlier, and adding the following code:
import React from "react";
import {
useDataGrid,
EditButton,
ShowButton,
DeleteButton,
List,
MarkdownField,
DateField,
} from "@refinedev/mui";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import {
IResourceComponentsProps,
useTranslate,
useMany,
} from "@refinedev/core";
import { Checkbox } from "@mui/material";
export const ProductList: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const { dataGridProps } = useDataGrid();
const { data: categoryData, isLoading: categoryIsLoading } = useMany({
resource: "categories",
ids: dataGridProps?.rows?.map((item: any) => item?.category?.id) ?? [],
queryOptions: {
enabled: !!dataGridProps?.rows,
},
});
const columns = React.useMemo<GridColDef[]>(
() => [
{
field: "id",
headerName: translate("id"),
minWidth: 50,
},
{
field: "name",
flex: 1,
headerName: translate("Name"),
minWidth: 200,
},
{
field: "price",
flex: 0.5,
headerName: translate("Price"),
},
{
field: "category",
flex: 0.5,
headerName: translate("Category"),
valueGetter: ({ row }) => {
const value = row?.category?.id;
return value;
},
renderCell: function render({ value }) {
return categoryIsLoading ? (
<>Loading...</>
) : (
categoryData?.data?.find((item) => item.id === value)?.title
);
},
},
{
field: "description",
flex: 1,
headerName: translate("Description"),
minWidth: 500,
renderCell: function render({ value }) {
return <MarkdownField value={(value ?? "").slice(0, 80) + "..."} />;
},
},
{
field: "actions",
headerName: translate("table.actions"),
sortable: false,
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</>
);
},
align: "center",
headerAlign: "center",
flex: 1,
},
],
[translate, categoryData?.data]
);
return (
<List>
<DataGrid {...dataGridProps} columns={columns} autoHeight />
</List>
);
};
The useMany
hook declaration in the code above fetches and renders the categories of each product in the categories
column on the table.
useMany
hook is useful when you want to fetch multiple records from the API. It will return the data and some functions to control the query.
It’s worth noting that the actions
column in the columns array renders icons for performing CRUD operations on each product in the table. When interacted with, these icons will route users to the edit
and show
pages, which we will create in the subsequent sections, or invoke a modal component for deleting individual items.
Next, create a index.ts
file in the src/pages/products
directory and export the list.tsx
file as follows:
export * from "./list";
Finally, in the src/App.tsx
file, import the list
component, add a products
resource object to the resources
array on the <Refine>
component, and add a products
route definition to the React Router DOM context, as highlighted in the code below:
...
import {
ProductCreate,
ProductEdit,
ProductList,
ProductShow,
} from "./pages/products";
function App() {
...
return (
<BrowserRouter>
...
<Refine
...
resources={[
{
name: "dashboard",
list: "/dashboard",
},
{
name: "products",
list: "/products",
create: "/products/create",
edit: "/products/edit/:id",
show: "/products/show/:id",
meta: {
canDelete: true,
},
},
]}
>
<Routes>
<Route>
...
<Route
index
element={<NavigateToResource resource="dashboard" />}
/>
<Route path="/dashboard">
<Route index element={<Dashboard />} />
</Route>
<Route path="/products">
<Route index element={<ProductList />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
</Refine>
</BrowserRouter>
);
}
export default App;
Note that we also added the create
, edit
, and show
actions to the products
resource and the products
route definition. This will prevent us from going back and forth when creating the rest of the pages.
Now, if you return to the browser, you should see the products
route in the sidebar and the list
page as shown in the GIF below.
The create
button in the upper right-hand corner of the page is an action button, as are the other action icons in the table. It redirects users to the create
page, where they can create new products.
Create Page
The create page does not exist as it is, so if you try to navigate to it using the create
action button on the list page, you will get redirected to a 404 page. Let us fix that.
Create a new file in the src/pages/products/create.tsx
directory, name it, and add the following code:
import { Create, useAutocomplete } from "@refinedev/mui";
import {
Box,
TextField,
Checkbox,
FormControlLabel,
Autocomplete,
} from "@mui/material";
import { useForm } from "@refinedev/react-hook-form";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Controller } from "react-hook-form";
export const ProductCreate: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const {
saveButtonProps,
refineCore: { formLoading },
register,
control,
formState: { errors },
} = useForm();
const { autocompleteProps: categoryAutocompleteProps } = useAutocomplete({
resource: "categories",
});
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("name", {
required: "This field is required",
})}
error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={translate("Name")}
name="name"
/>
<Controller
control={control}
name="isActive"
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<FormControlLabel
label={translate("isActive")}
control={
<Checkbox
{...field}
checked={field.value}
onChange={(event) => {
field.onChange(event.target.checked);
}}
/>
}
/>
)}
/>
<TextField
{...register("description", {
required: "This field is required",
})}
error={!!(errors as any)?.description}
helperText={(errors as any)?.description?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={translate("description")}
name="description"
/>
<TextField
{...register("price", {
required: "This field is required",
valueAsNumber: true,
})}
error={!!(errors as any)?.price}
helperText={(errors as any)?.price?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={translate("Price")}
name="price"
/>
<Controller
control={control}
name="category"
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...categoryAutocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value);
}}
getOptionLabel={(item) => {
return (
categoryAutocompleteProps?.options?.find(
(p) => p?.id?.toString() === item?.id?.toString()
)?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) =>
value === undefined ||
option?.id?.toString() === value?.id?.toString()
}
renderInput={(params) => (
<TextField
{...params}
label={translate("Category")}
margin="normal"
variant="outlined"
error={!!(errors as any)?.category?.id}
helperText={(errors as any)?.category?.id?.message}
required
/>
)}
/>
)}
/>
</Box>
</Create>
);
};
In this example, we used the useForm
hook and Create
component from refine, in conjunction with Material UI components, to post data, create a layout, and construct a form, respectively.
The Create page’s key component is using the useForm
hook from the @refinedev/react-hook-form package. This hook combines the capabilities of the useForm
hook from the Refine core, which manages form submission, data retrieval, caching, state control, and server-side error handling. The integration of React Hook Form enhances form functionality by improving form field state management and error handling.
To learn more about these hooks and components, refer to refine’s documentation.
To finish the setup, go to the pages/products/index.tsx
file and export the component as follows:
export * from “./list”;
export * from “./create”;
Since we have added the page resource and route to the resources and product route definition earlier, we can skip that step and view the result in the browser.
Edit Page
The <ProductEdit />
page is where users are redirected when interacting with the edit icon in the action column on the table. The edit
page will have a similar code to the create
page, but instead of using the Create
component, we will use the Edit
component from Refine.
To create the edit
page, create a new file inside the src/pages/products
directory and name it edit.tsx
and add the following code:
import { Edit, useAutocomplete } from "@refinedev/mui";
import {
Box,
TextField,
Checkbox,
FormControlLabel,
Autocomplete,
} from "@mui/material";
import { useForm } from "@refinedev/react-hook-form";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Controller } from "react-hook-form";
export const ProductEdit: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const {
saveButtonProps,
refineCore: { queryResult },
register,
control,
formState: { errors },
} = useForm();
const productsData = queryResult?.data?.data;
const { autocompleteProps: categoryAutocompleteProps } = useAutocomplete({
resource: "categories",
defaultValue: productsData?.category?.id,
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("id", {
required: "This field is required",
valueAsNumber: true,
})}
error={!!(errors as any)?.id}
helperText={(errors as any)?.id?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={translate("id")}
name="id"
disabled
/>
<TextField
{...register("name", {
required: "This field is required",
})}
error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={translate("Name")}
name="name"
/>
<Controller
control={control}
name="isActive"
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<FormControlLabel
label={translate("IsActive")}
control={
<Checkbox
{...field}
checked={field.value}
onChange={(event) => {
field.onChange(event.target.checked);
}}
/>
}
/>
)}
/>
<TextField
{...register("description", {
required: "This field is required",
})}
error={!!(errors as any)?.description}
helperText={(errors as any)?.description?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={translate("Description")}
name="description"
/>
<TextField
{...register("price", {
required: "This field is required",
valueAsNumber: true,
})}
error={!!(errors as any)?.price}
helperText={(errors as any)?.price?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={translate("Price")}
name="price"
/>
<Controller
control={control}
name="category"
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...categoryAutocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value);
}}
getOptionLabel={(item) => {
return (
categoryAutocompleteProps?.options?.find(
(p) => p?.id?.toString() === item?.id?.toString()
)?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) =>
value === undefined ||
option?.id?.toString() === value?.id?.toString()
}
renderInput={(params) => (
<TextField
{...params}
label={translate("Category")}
margin="normal"
variant="outlined"
error={!!(errors as any)?.category?.id}
helperText={(errors as any)?.category?.id?.message}
required
/>
)}
/>
)}
/>
</Box>
</Edit>
);
};
Next, open the src/pages/products/.index.tsx
and export the file like so:
export * from "./list";
export * from "./create";
export * from "./edit";
Now, the edit
page should render properly when you click on the action button.
Show Page
Like the previous pages, refine offers a dedicated component for the show page, known as the Show component. However, unlike the other two pages, the Show component is designed solely for displaying individual products. It also incorporates action buttons that allow users to navigate to the edit page and delete the product with a click.
The show
page will have a simple structure. It will fetch details of an individual product from the API using Refine’s useShow
hook and render them using Material UI components, which are encompassed by the Show
component.
To create this page, go back to the src/pages/products
directory and create a new file, name it show.tsx
and add the following code:
import {
useShow,
IResourceComponentsProps,
useTranslate,
useOne,
} from "@refinedev/core";
import {
Show,
NumberField,
TextFieldComponent as TextField,
BooleanField,
MarkdownField,
DateField,
} from "@refinedev/mui";
import { Typography, Stack } from "@mui/material";
export const ProductShow: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const { queryResult } = useShow();
const { data, isLoading } = queryResult;
const record = data?.data;
const { data: categoryData, isLoading: categoryIsLoading } = useOne({
resource: "categories",
id: record?.category?.id || "",
queryOptions: {
enabled: !!record,
},
});
return (
<Show isLoading={isLoading}>
<Stack gap={1}>
<Typography variant="body1" fontWeight="bold">
{translate("id")}
</Typography>
<NumberField value={record?.id ?? ""} />
<Typography variant="body1" fontWeight="bold">
{translate("Name")}
</Typography>
<TextField value={record?.name} />
<Typography variant="body1" fontWeight="bold">
{translate("IsActive")}
</Typography>
<BooleanField value={record?.isActive} />
<Typography variant="body1" fontWeight="bold">
{translate("Description")}
</Typography>
<MarkdownField value={record?.description} />
<Typography variant="body1" fontWeight="bold">
{translate("CreatedAt")}
</Typography>
<DateField value={record?.createdAt} />
<Typography variant="body1" fontWeight="bold">
{translate("Price")}
</Typography>
<NumberField value={record?.price ?? ""} />
<Typography variant="body1" fontWeight="bold">
{translate("Category")}
</Typography>
{categoryIsLoading ? <>Loading...</> : <>{categoryData?.data?.title}</>}
</Stack>
</Show>
);
};
Finally, go to the src/pages/products/index.tsx
file and export the file as we’ve done with the previous pages.
export * from "./list";
export * from "./create";
export * from "./edit";
export * from "./show";
This concludes the products
resource pages. The pages should work together seamlessly when interacting with them in the browser.
Categories pages
The categories
CRUD pages are pretty similar to the products
pages, sharing the same code except for the data source. In this case, we’ll query data from the API’s /categories
endpoint instead of /products
.
As a result, we won’t spend much time on this and will quickly go through the process of setting up the pages. First, let’s create the src/pages/categories
directory and add all our files.
To do this, create the directory and add the following files:
CategoryList.tsx
CategoryCreate.tsx
CategoryEdit.tsx
CategoryShow.tsx
index.ts
Next, navigate to the src/App.tsx
file and update the resources
and routes
with the highlighted code:
...
import {
CategoryCreate,
CategoryEdit,
CategoryList,
CategoryShow,
} from "./pages/categories";
import { Dashboard } from "./pages/dashboard";
import {
ProductCreate,
ProductEdit,
ProductList,
ProductShow,
} from "./pages/products";
function App() {
...
return (
<BrowserRouter>
<Refine
dataProvider={dataProvider("https://api.finefoods.refine.dev")}
notificationProvider={notificationProvider}
i18nProvider={i18nProvider}
routerProvider={routerBindings}
resources={[
{
name: "dashboard",
list: "/dashboard",
},
{
name: "categories",
list: "/categories",
create: "/categories/create",
edit: "/categories/edit/:id",
show: "/categories/show/:id",
meta: {
canDelete: true,
},
},
{
name: "products",
list: "/products",
create: "/products/create",
edit: "/products/edit/:id",
show: "/products/show/:id",
meta: {
canDelete: true,
},
},
]}
>
<Routes>
<Route>
...
<Route
index
element={<NavigateToResource resource="dashboard" />}
/>
<Route path="/dashboard">
<Route index element={<Dashboard />} />
</Route>
<Route path="/categories">
<Route index element={<CategoryList />} />
<Route path="create" element={<CategoryCreate />} />
<Route path="edit/:id" element={<CategoryEdit />} />
<Route path="show/:id" element={<CategoryShow />} />
</Route>
<Route path="/products">
<Route index element={<ProductList />} />
<Route path="create" element={<ProductCreate />} />
<Route path="edit/:id" element={<ProductEdit />} />
<Route path="show/:id" element={<ProductShow />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
</Refine>
</BrowserRouter>
);
}
export default App;
List page
We’ll start with the list
page. Navigate to the CategoryList.tsx
file and add the following code:
import React from "react";
import {
useDataGrid,
EditButton,
ShowButton,
DeleteButton,
List,
MarkdownField,
} from "@refinedev/mui";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
export const CategoryList: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const { dataGridProps } = useDataGrid();
const columns = React.useMemo<GridColDef[]>(
() => [
{
field: "id",
headerName: translate("categories.fields.id"),
width: 200,
},
{
field: "title",
headerName: translate("categories.fields.title"),
flex: 1,
},
{
field: "actions",
headerName: translate("table.actions"),
sortable: false,
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</>
);
},
align: "center",
headerAlign: "center",
flex: 1,
},
],
[translate]
);
return (
<List>
<DataGrid {...dataGridProps} columns={columns} autoHeight />
</List>
);
};
As you can see, the code is not significantly different from the code we used in the products list
page earlier.
After adding the code to the page, go to the index.js
file in the root directory of the categories
folder and export the components like this:
export { CategoryCreate } from "./create";
export { CategoryEdit } from "./edit";
export { CategoryList } from "./list";
export { CategoryShow } from "./show";
Create Page
We’ll do the same for the CategoryCreate
page. Open the file and add the following code:
import { Create } from "@refinedev/mui";
import { Box, TextField, Checkbox, FormControlLabel } from "@mui/material";
import { useForm } from "@refinedev/react-hook-form";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Controller } from "react-hook-form";
export const CategoryCreate: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const {
saveButtonProps,
refineCore: { formLoading },
register,
control,
formState: { errors },
} = useForm();
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={translate("categories.fields.title")}
name="title"
/>
<Controller
control={control}
name="isActive"
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<FormControlLabel
label={translate("isActive")}
control={
<Checkbox
{...field}
checked={field.value}
onChange={(event) => {
field.onChange(event.target.checked);
}}
/>
}
/>
)}
/>
<TextField
{...register("cover", {
required: "This field is required",
})}
error={!!(errors as any)?.cover}
helperText={(errors as any)?.cover?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={translate("cover")}
name="cover"
/>
</Box>
</Create>
);
};
Edit page
Likewise, the edit
page. Open the CategoryEdit.tsx
file and add the following code:
import { Edit } from "@refinedev/mui";
import { Box, TextField, Checkbox, FormControlLabel } from "@mui/material";
import { useForm } from "@refinedev/react-hook-form";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Controller } from "react-hook-form";
export const CategoryEdit: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const {
saveButtonProps,
register,
control,
formState: { errors },
} = useForm();
return (
<Edit saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("id", {
required: "This field is required",
valueAsNumber: true,
})}
error={!!(errors as any)?.id}
helperText={(errors as any)?.id?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={translate("categories.fields.id")}
name="id"
disabled
/>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={translate("categories.fields.title")}
name="title"
/>
<Controller
control={control}
name="isActive"
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<FormControlLabel
label={translate("isActive")}
control={
<Checkbox
{...field}
checked={field.value}
onChange={(event) => {
field.onChange(event.target.checked);
}}
/>
}
/>
)}
/>
</Box>
</Edit>
);
};
Show Page
Finally, to complete the step, copy and paste the code below into the CategoryShow.tsx
file:
import {
useShow,
IResourceComponentsProps,
useTranslate,
} from "@refinedev/core";
import {
Show,
NumberField,
TextFieldComponent as TextField,
BooleanField,
} from "@refinedev/mui";
import { Typography, Stack } from "@mui/material";
export const CategoryShow: React.FC<IResourceComponentsProps> = () => {
const translate = useTranslate();
const { queryResult } = useShow();
const { data, isLoading } = queryResult;
const record = data?.data;
return (
<Show isLoading={isLoading}>
<Stack gap={1}>
<Typography variant="body1" fontWeight="bold">
{translate("categories.fields.id")}
</Typography>
<NumberField value={record?.id ?? ""} />
<Typography variant="body1" fontWeight="bold">
{translate("categories.fields.title")}
</Typography>
<TextField value={record?.title} />
<Typography variant="body1" fontWeight="bold">
{translate("isActive")}
</Typography>
<BooleanField value={record?.isActive} />
</Stack>
</Show>
);
};
With every page in the src/pages/categories
folder appropriately populated, we can go ahead and view the finished product in the browser.
What is Refine Inferencer
As previously discussed, Refine auto-generate CRUD pages based on the backend architecture and API your application is bootstrapped with. This is done using Refine’s inferencer
package.
The inferencer
package is a nifty tool the refine team developed to allow developers to auto-generate CRUD pages for resources, such as the categories
and products
pages, based on the data structure.
The package was created to help developers reduce the amount of time spent on creating pages for resources. It exports CRUD pages such as List
, Show
, Create
, and Edit
pages based on the design system scope of your application.
This means that it creates the highlighted pages using UI elements that your application is configured to use. For example, suppose you choose the Ant Design system for your project when bootstrapping it using the app scaffolder. In that case, the Inferencer will automatically generate codes for each page using Ant Design UI elements.
The Inferencer package is available for every design system refine is compatible with, such as:
- Ant Design
- Material UI
- Chakra UI
- Mantine
- Headless
For more information on the Inferencer
package, visit the refine documentation.
Conclusion
As we have seen in this article, refine makes building data-intensive applications a breeze by providing several hooks and packages that can halve the time it takes to build such applications compared to traditional tools and methods.