Skip to main content
While Refine excels at CRUD operations, real-world applications often need custom pages like dashboards, reports, settings, and more. This guide shows you how to create custom pages that integrate seamlessly with Refine’s ecosystem.

Basic Custom Page

Custom pages are just regular React components. You can create them anywhere in your application:
// pages/dashboard.tsx
import { useCustom } from "@refinedev/core";

export const DashboardPage = () => {
  const { data, isLoading } = useCustom({
    url: `${API_URL}/statistics`,
    method: "get",
  });

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <div>
        <div>Total Posts: {data?.data.totalPosts}</div>
        <div>Total Users: {data?.data.totalUsers}</div>
      </div>
    </div>
  );
};

Registering Custom Routes

With React Router

import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Refine } from "@refinedev/core";
import routerProvider from "@refinedev/react-router";
import { DashboardPage } from "./pages/dashboard";

const App = () => {
  return (
    <BrowserRouter>
      <Refine
        routerProvider={routerProvider}
        resources={[
          {
            name: "posts",
            list: "/posts",
          },
        ]}
      >
        <Routes>
          <Route
            element={
              <Layout>
                <Outlet />
              </Layout>
            }
          >
            <Route path="/dashboard" element={<DashboardPage />} />
            <Route path="/posts" element={<PostList />} />
          </Route>
        </Routes>
      </Refine>
    </BrowserRouter>
  );
};

With Next.js App Router

// app/dashboard/page.tsx
"use client";

import { useCustom } from "@refinedev/core";

export default function DashboardPage() {
  const { data, isLoading } = useCustom({
    url: `${API_URL}/statistics`,
    method: "get",
  });

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Your dashboard content */}
    </div>
  );
}

With Remix

// app/routes/dashboard.tsx
import { useCustom } from "@refinedev/core";

export default function DashboardRoute() {
  const { data, isLoading } = useCustom({
    url: `${API_URL}/statistics`,
    method: "get",
  });

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Your dashboard content */}
    </div>
  );
}

Adding Custom Pages to Menu

To show custom pages in the sidebar menu, you can use the meta.label property or customize the sider:

Using Resource Configuration

const App = () => {
  return (
    <Refine
      resources={[
        {
          name: "dashboard",
          list: "/dashboard",
          meta: {
            label: "Dashboard",
            icon: <DashboardOutlined />,
          },
        },
        {
          name: "posts",
          list: "/posts",
        },
      ]}
    >
      {/* ... */}
    </Refine>
  );
};

Custom Sider with Menu Items

import { useMenu } from "@refinedev/core";
import { NavLink } from "react-router-dom";

export const CustomSider = () => {
  const { menuItems } = useMenu();

  return (
    <aside>
      <nav>
        <NavLink to="/dashboard">
          Dashboard
        </NavLink>
        
        {menuItems.map((item) => (
          <NavLink key={item.key} to={item.route || "/"}>
            {item.icon}
            {item.label}
          </NavLink>
        ))}
        
        <NavLink to="/settings">
          Settings
        </NavLink>
      </nav>
    </aside>
  );
};

Dashboard with Statistics

A common use case is creating a dashboard with various statistics:
import { useCustom, useList } from "@refinedev/core";
import { Card, Row, Col, Statistic } from "antd";

export const DashboardPage = () => {
  // Get recent posts
  const { data: posts } = useList({
    resource: "posts",
    config: {
      pagination: { pageSize: 5 },
      sort: [{ field: "createdAt", order: "desc" }],
    },
  });

  // Get statistics from custom endpoint
  const { data: stats } = useCustom({
    url: `${API_URL}/statistics`,
    method: "get",
  });

  return (
    <div>
      <h1>Dashboard</h1>
      
      <Row gutter={16}>
        <Col span={6}>
          <Card>
            <Statistic
              title="Total Posts"
              value={stats?.data.totalPosts}
            />
          </Card>
        </Col>
        <Col span={6}>
          <Card>
            <Statistic
              title="Total Users"
              value={stats?.data.totalUsers}
            />
          </Card>
        </Col>
        <Col span={6}>
          <Card>
            <Statistic
              title="Total Views"
              value={stats?.data.totalViews}
            />
          </Card>
        </Col>
        <Col span={6}>
          <Card>
            <Statistic
              title="Active Users"
              value={stats?.data.activeUsers}
            />
          </Card>
        </Col>
      </Row>

      <Card title="Recent Posts" style={{ marginTop: 16 }}>
        <ul>
          {posts?.data.map((post) => (
            <li key={post.id}>{post.title}</li>
          ))}
        </ul>
      </Card>
    </div>
  );
};

Settings Page

A settings page typically updates user preferences:
import { useCustomMutation, useGetIdentity } from "@refinedev/core";
import { Form, Input, Button, Switch } from "antd";

export const SettingsPage = () => {
  const { data: user } = useGetIdentity();
  const { mutate, isLoading } = useCustomMutation();

  const onFinish = (values: any) => {
    mutate({
      url: `${API_URL}/users/${user?.id}/settings`,
      method: "patch",
      values,
      successNotification: {
        message: "Settings updated successfully",
        type: "success",
      },
    });
  };

  return (
    <div>
      <h1>Settings</h1>
      <Form
        layout="vertical"
        onFinish={onFinish}
        initialValues={user?.settings}
      >
        <Form.Item label="Name" name="name">
          <Input />
        </Form.Item>
        
        <Form.Item label="Email" name="email">
          <Input type="email" />
        </Form.Item>
        
        <Form.Item
          label="Email Notifications"
          name="emailNotifications"
          valuePropName="checked"
        >
          <Switch />
        </Form.Item>
        
        <Form.Item>
          <Button type="primary" htmlType="submit" loading={isLoading}>
            Save Settings
          </Button>
        </Form.Item>
      </Form>
    </div>
  );
};

Reports Page

Generate and display reports with charts:
import { useCustom } from "@refinedev/core";
import { Card, DatePicker, Button } from "antd";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from "recharts";
import { useState } from "react";

export const ReportsPage = () => {
  const [dateRange, setDateRange] = useState([null, null]);
  
  const { data, isLoading, refetch } = useCustom({
    url: `${API_URL}/reports/sales`,
    method: "get",
    config: {
      query: {
        startDate: dateRange[0]?.format("YYYY-MM-DD"),
        endDate: dateRange[1]?.format("YYYY-MM-DD"),
      },
    },
  });

  return (
    <div>
      <h1>Sales Report</h1>
      
      <Card style={{ marginBottom: 16 }}>
        <DatePicker.RangePicker
          onChange={(dates) => setDateRange(dates)}
          value={dateRange}
        />
        <Button
          type="primary"
          onClick={() => refetch()}
          loading={isLoading}
          style={{ marginLeft: 8 }}
        >
          Generate Report
        </Button>
      </Card>

      {data && (
        <Card>
          <LineChart width={800} height={400} data={data.data}>
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="date" />
            <YAxis />
            <Tooltip />
            <Line type="monotone" dataKey="sales" stroke="#8884d8" />
          </LineChart>
        </Card>
      )}
    </div>
  );
};

Profile Page

Display and edit user profile information:
import { useGetIdentity, useCustomMutation } from "@refinedev/core";
import { Form, Input, Button, Upload, Avatar } from "antd";
import { UserOutlined, UploadOutlined } from "@ant-design/icons";

export const ProfilePage = () => {
  const { data: user } = useGetIdentity();
  const { mutate, isLoading } = useCustomMutation();

  const onFinish = (values: any) => {
    mutate({
      url: `${API_URL}/users/${user?.id}`,
      method: "patch",
      values,
    });
  };

  return (
    <div>
      <h1>Profile</h1>
      
      <div style={{ marginBottom: 24 }}>
        <Avatar size={100} icon={<UserOutlined />} src={user?.avatar} />
      </div>

      <Form
        layout="vertical"
        onFinish={onFinish}
        initialValues={user}
      >
        <Form.Item label="Name" name="name">
          <Input />
        </Form.Item>
        
        <Form.Item label="Email" name="email">
          <Input type="email" disabled />
        </Form.Item>
        
        <Form.Item label="Bio" name="bio">
          <Input.TextArea rows={4} />
        </Form.Item>
        
        <Form.Item label="Avatar" name="avatar">
          <Upload>
            <Button icon={<UploadOutlined />}>Upload Avatar</Button>
          </Upload>
        </Form.Item>
        
        <Form.Item>
          <Button type="primary" htmlType="submit" loading={isLoading}>
            Update Profile
          </Button>
        </Form.Item>
      </Form>
    </div>
  );
};

Using Custom Hooks

Create reusable hooks for custom pages:
// hooks/useStatistics.ts
import { useCustom } from "@refinedev/core";

export const useStatistics = () => {
  return useCustom({
    url: `${API_URL}/statistics`,
    method: "get",
    queryOptions: {
      // Cache for 5 minutes
      cacheTime: 5 * 60 * 1000,
      staleTime: 5 * 60 * 1000,
    },
  });
};

// Use in component
const DashboardPage = () => {
  const { data, isLoading } = useStatistics();
  // ...
};

Access Control for Custom Pages

Protect custom pages with access control:
import { CanAccess } from "@refinedev/core";

export const AdminDashboard = () => {
  return (
    <CanAccess
      resource="admin-dashboard"
      action="view"
      fallback={<div>Access Denied</div>}
    >
      <div>
        <h1>Admin Dashboard</h1>
        {/* Admin-only content */}
      </div>
    </CanAccess>
  );
};

Layout Integration

Custom pages should use your application’s layout:
import { ThemedLayout } from "@refinedev/antd";

const App = () => {
  return (
    <Refine {...refineoptions}>
      <Routes>
        <Route
          element={
            <ThemedLayout>
              <Outlet />
            </ThemedLayout>
          }
        >
          <Route path="/dashboard" element={<DashboardPage />} />
          <Route path="/settings" element={<SettingsPage />} />
          <Route path="/profile" element={<ProfilePage />} />
          {/* Resource routes */}
        </Route>
      </Routes>
    </Refine>
  );
};

Best Practices

  1. Use Refine hooks: Leverage useCustom, useCustomMutation for data fetching
  2. Consistent styling: Match your custom pages with resource pages
  3. Loading states: Always show loading indicators
  4. Error handling: Handle errors gracefully
  5. Breadcrumbs: Add breadcrumbs for navigation
  6. Access control: Protect sensitive custom pages
  7. SEO: Set proper page titles and meta tags
  8. Mobile responsive: Ensure pages work on all devices

Further Reading

Build docs developers (and LLMs) love