Skip to main content
Version: 3.xx.xx

Custom Pages

caution

This document is related to how to create custom pages for react applications. Since Nextjs and Remix have a file system based router built on the page concept, you can create your custom pages under the pages or routes folder.

Refer to the Nextjs Guide documentation for detailed information.

Refer to the Remix Guide documentation for detailed information.


refine allows us to add custom pages to our application. To do this, it is necessary to create an object array with react-router-dom <Route> properties. Then, pass this array as routes property in routerProvider property.

tip

When you create a custom page, it will not be visible in the <Sider /> component. You can trick the <Sider/> by passing an empty resource to show your custom page.

Example
const App = () => (
<Refine
resources={[
// This will add an item to `<Sider/>` with route `/my-custom-item`
{ name: "my-custom-item", list: () => null },
]}
/>
);

Public Custom Pages

Allows creating custom pages that everyone can access via path.

src/App.tsx
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";

import { CustomPage } from "pages/custom-page";

const App = () => {
return (
<Refine
...
routerProvider={{
...routerProvider,
routes: [
{
element: <CustomPage />,
path: "/custom-page",
},
],
}}
/>
);
};

export default App;

Everyone can access this page via /custom-page path.

Authenticated Custom Pages

Allows creating custom pages that only authenticated users can access via path.

src/App.tsx
import { Refine, Authenticated, AuthProvider } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";

import { CustomPage } from "pages/custom-page";

const authProvider: AuthProvider = {
login: (params: any) => {
if (params.username === "admin") {
localStorage.setItem("username", params.username);
return Promise.resolve();
}

return Promise.reject();
},
logout: () => {
localStorage.removeItem("username");
return Promise.resolve();
},
checkError: () => Promise.resolve(),
checkAuth: () =>
localStorage.getItem("username") ? Promise.resolve() : Promise.reject(),
getPermissions: () => Promise.resolve(["admin"]),
};

const AuthenticatedCustomPage = () => {
return (
<Authenticated>
<CustomPage />
</Authenticated>
);
};

const App = () => {
return (
<Refine
...
authProvider={authProvider}
routerProvider={{
...routerProvider,
routes: [
{
element: <AuthenticatedCustomPage />,
path: "/custom-page",
},
],
}}
/>
);
};

export default App;

Only authenticated users can access this page via /custom-page path.

attention

For authenticated custom page, your application needs an authProvider.

Refer to the authProvider for more detailed information.

Layout for Custom Pages

import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";

import { CustomPage } from "pages/custom-page";

const App = () => {
return (
<Refine
...
routerProvider={{
...routerProvider,
routes: [
{
element: <CustomPage />,
path: "/custom-page",
layout: true
},
],
}}
/>
);
};

export default App;

Example

Let's make a custom page for posts. On this page, the editor can approve or reject the posts with the "draft" status.

Before starting the example, let's assume that our dataProvider has an endpoint that returns posts as follows.

https://api.fake-rest.refine.dev/posts
{
[
{
id: 1,
title: "Dolorem suscipit assumenda laborum id facilis maiores.",
content:
"Non et asperiores dolores. Vero quas natus sed ut iste omnis sequi. Enim veniam soluta vel. Est soluta suscipit velit architecto et. Tenetur ea impedit alias rerum in tenetur. Aut tempore consequatur ipsa neque aspernatur sit. Ut ea aspernatur aut voluptatem. Nulla quos laboriosam molestiae impedit eius. Dicta est maxime fuga debitis. Dicta necessitatibus odit quis qui animi.",
category: {
id: 32,
},
status: "draft",
},
{
id: 2,
title: "Voluptatibus laboriosam dignissimos non.",
content:
"Dolor cumque blanditiis aspernatur earum quo autem voluptatem vel consequuntur. Consequatur et sed dolores rerum ipsam aut et sed. Nostrum provident voluptas facere distinctio voluptates in et. Magni asperiores quod unde tempore veritatis beatae qui cum officia. Omnis quia cumque et qui. Quis et explicabo et similique voluptatum. Culpa assumenda autem laborum impedit perspiciatis ducimus perferendis. Quo doloribus magnam perferendis doloremque voluptas libero autem. Nihil enim aliquam molestias aspernatur impedit. Ad eius qui sit et.",
category: {
id: 22,
},
status: "draft",
},
// ...
];
}

First, we will create the post's CRUD pages and bootstrap the app.

src/App.tsx
import { Refine } from "@pankod/refine-core";
import dataProvider from "@pankod/refine-simple-rest";
import routerProvider from "@pankod/refine-react-router-v6";

import "@pankod/refine-antd/dist/reset.css";

import { PostList, PostCreate, PostEdit, PostShow } from "pages/posts";

const App = () => {
return (
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
resources={[
{
name: "posts",
list: PostList,
create: PostCreate,
edit: PostEdit,
show: PostShow,
},
]}
/>
);
};

export default App;

Now, let's create the custom page with the name <PostReview>. We will use the properties of useList, filter, and pagination to fetch a post with "draft" status.

Refer to the useList documentation for detailed usage.

src/pages/post-review.tsx
import { useList } from "@pankod/refine-core";

const PostReview = () => {
const { data, isLoading } = useList<IPost>({
resource: "posts",
config: {
filters: [
{
field: "status",
operator: "eq",
value: "draft",
},
],
pagination: { pageSize: 1 },
},
});
};

interface ICategory {
id: number;
title: string;
}

interface IPost {
id: number;
title: string;
content: string;
status: "published" | "draft" | "rejected";
category: { id: number };
}

We set the filtering process with filters then the page size set with pagination to return only one post.

Post's category is relational. So we will use the post's category "id" to get the category title. Let's use useOne to fetch the category we want.

src/pages/post-review.tsx
import { useList, useOne } from "@pankod/refine-core";

export const PostReview = () => {
const { data, isLoading } = useList<IPost>({
resource: "posts",
config: {
filters: [
{
field: "status",
operator: "eq",
value: "draft",
},
],
pagination: { pageSize: 1 },
},
});

const post = data?.data[0];

const { data: categoryData, isLoading: categoryIsLoading } =
useOne<ICategory>({
resource: "categories",
id: post!.category.id,
queryOptions: {
enabled: !!post,
},
});
};

Now we have the data to display the post as we want. Let's use the <Show> component of refine to show this data.

tip

<Show> component is not required, you are free to display the data as you wish.

src/pages/post-review.tsx
import { useOne, useList } from "@pankod/refine-core";
import {
Typography,
Show,
MarkdownField,
} from "@pankod/refine-antd";

const { Title, Text } = Typography;

export const PostReview = () => {
const { data, isLoading } = useList<IPost>({
resource: "posts",
config: {
filters: [
{
field: "status",
operator: "eq",
value: "draft",
},
],
pagination: { pageSize: 1 },
},
});
const record = data?.data[0];

const { data: categoryData, isLoading: categoryIsLoading } =
useOne<ICategory>({
resource: "categories",
id: record!.category.id,
queryOptions: {
enabled: !!record,
},
});

return (
<Show
title="Review Posts"
resource="posts"
recordItemId={record?.id}
isLoading={isLoading || categoryIsLoading}
headerProps={{
backIcon: false,
}}
>
<Title level={5}>Status</Title>
<Text>{record?.status}</Text>
<Title level={5}>Title</Title>
<Text>{record?.title}</Text>
<Title level={5}>Category</Title>
<Text>{categoryData?.data.title}</Text>
<Title level={5}>Content</Title>
<MarkdownField value={record?.content} />
</Show>
);
};

Then, pass this <PostReview> as the routes property in the <Refine> component:

src/App.tsx
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import dataProvider from "@pankod/refine-simple-rest";

import "@pankod/refine-antd/dist/reset.css";

import { PostList, PostCreate, PostEdit, PostShow } from "pages/posts";

import { PostReview } from "pages/post-review";

const App = () => {
return (
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
routerProvider={{
...routerProvider,
routes: [
{
element: <PostReview />,
path: "/post-review",
},
],
}}
resources={[
{
name: "posts",
list: PostList,
create: PostCreate,
edit: PostEdit,
show: PostShow,
},
]}
/>
);
};

export default App;

Now our page looks like this:

A custom page

Now let's put in approve and reject buttons to change the status of the post shown on the page. When these buttons are clicked, we will change the status of the post using useUpdate.

Refer to the useUpdate documentation for detailed usage.

src/pages/post-review.tsx
import {
useList,
useOne,
useUpdate,
} from "@pankod/refine-core";
import {
Typography,
Show,
MarkdownField,
Space,
Button,
} from "@pankod/refine-antd";

const { Title, Text } = Typography;

export const PostReview = () => {
const { data, isLoading } = useList<IPost>({
resource: "posts",
config: {
filters: [
{
field: "status",
operator: "eq",
value: "draft",
},
],
pagination: { pageSize: 1 },
},
});
const record = data?.data[0];

const { data: categoryData, isLoading: categoryIsLoading } =
useOne<ICategory>({
resource: "categories",
id: record!.category.id,
queryOptions: {
enabled: !!record,
},
});

const mutationResult = useUpdate<IPost>();

const { mutate, isLoading: mutateIsLoading } = mutationResult;

const handleUpdate = (item: IPost, status: string) => {
mutate({ resource: "posts", id: item.id, values: { ...item, status } });
};

const buttonDisabled = isLoading || categoryIsLoading || mutateIsLoading;

return (
<Show
title="Review Posts"
resource="posts"
recordItemId={record?.id}
isLoading={isLoading || categoryIsLoading}
headerProps={{
backIcon: false,
}}
headerButtons={
<Space
key="action-buttons"
style={{ float: "right", marginRight: 24 }}
>
<Button
danger
disabled={buttonDisabled}
onClick={() =>
record && handleUpdate(record, "rejected")
}
>
Reject
</Button>
<Button
type="primary"
disabled={buttonDisabled}
onClick={() =>
record && handleUpdate(record, "published")
}
>
Approve
</Button>
</Space>
}
>
<Title level={5}>Status</Title>
<Text>{record?.status}</Text>
<Title level={5}>Title</Title>
<Text>{record?.title}</Text>
<Title level={5}>Category</Title>
<Text>{categoryData?.data.title}</Text>
<Title level={5}>Content</Title>
<MarkdownField value={record?.content} />
</Show>
);
};
A custom page in action

Example

RUN IN YOUR LOCAL
npm create refine-app@latest -- --example with-custom-pages