Refine Admin Panel Development
Refine is a headless React framework for admin interfaces. Unlike React Admin, which provides Material UI components, Refine separates logic and UI: the core manages state, routing, data fetching, permissions — while the developer chooses UI components (Ant Design, Material UI, Chakra, Mantine, or fully custom).
Installation
npm create refine-app@latest -- --preset refine-nextjs
# or for Vite + Ant Design:
npm create refine-app@latest -- --preset refine-vite
Manual installation for existing project:
npm install @refinedev/core @refinedev/react-router-v6
# UI package of choice:
npm install @refinedev/antd antd
# or:
npm install @refinedev/mui @mui/material @emotion/react
Project Structure
src/
App.tsx
providers/
dataProvider.ts
authProvider.ts
pages/
users/
list.tsx
edit.tsx
create.tsx
show.tsx
products/
list.tsx
edit.tsx
App.tsx
import { Refine } from '@refinedev/core';
import { RefineThemes, ThemedLayoutV2 } from '@refinedev/antd';
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
import { ConfigProvider } from 'antd';
import ruRU from 'antd/locale/ru_RU';
import { dataProvider } from './providers/dataProvider';
import { authProvider } from './providers/authProvider';
import { UserList, UserEdit, UserCreate } from './pages/users';
import { ProductList, ProductEdit } from './pages/products';
export function App() {
return (
<BrowserRouter>
<ConfigProvider theme={RefineThemes.Blue} locale={ruRU}>
<Refine
dataProvider={dataProvider}
authProvider={authProvider}
routerProvider={routerBindings}
resources={[
{
name: 'users',
list: '/users',
create: '/users/create',
edit: '/users/edit/:id',
show: '/users/show/:id',
meta: { label: 'Users', icon: <UserOutlined /> },
},
{
name: 'products',
list: '/products',
edit: '/products/edit/:id',
meta: { label: 'Products' },
},
]}
options={{ syncWithLocation: true }}
>
<Routes>
<Route element={<ThemedLayoutV2><Outlet /></ThemedLayoutV2>}>
<Route path="/users" element={<UserList />} />
<Route path="/users/create" element={<UserCreate />} />
<Route path="/users/edit/:id" element={<UserEdit />} />
<Route path="/products" element={<ProductList />} />
<Route path="/products/edit/:id" element={<ProductEdit />} />
</Route>
</Routes>
</Refine>
</ConfigProvider>
</BrowserRouter>
);
}
dataProvider
Refine uses standardized dataProvider interface:
import { DataProvider } from '@refinedev/core';
import axios from 'axios';
const api = axios.create({ baseURL: import.meta.env.VITE_API_URL });
export const dataProvider: DataProvider = {
getList: async ({ resource, pagination, sorters, filters }) => {
const { current = 1, pageSize = 10 } = pagination ?? {};
const sortParams = sorters?.reduce((acc, s) => ({
...acc,
[`sort[${s.field}]`]: s.order,
}), {});
const filterParams = filters?.reduce((acc, f) => {
if (f.operator === 'eq') return { ...acc, [f.field]: f.value };
if (f.operator === 'contains') return { ...acc, [`${f.field}_like`]: f.value };
return acc;
}, {});
const { data } = await api.get(`/${resource}`, {
params: {
_page: current,
_limit: pageSize,
...sortParams,
...filterParams,
},
});
return {
data: data.items,
total: data.total,
};
},
getOne: async ({ resource, id }) => {
const { data } = await api.get(`/${resource}/${id}`);
return { data };
},
create: async ({ resource, variables }) => {
const { data } = await api.post(`/${resource}`, variables);
return { data };
},
update: async ({ resource, id, variables }) => {
const { data } = await api.patch(`/${resource}/${id}`, variables);
return { data };
},
deleteOne: async ({ resource, id }) => {
const { data } = await api.delete(`/${resource}/${id}`);
return { data };
},
getApiUrl: () => import.meta.env.VITE_API_URL,
};
List with useTable
Refine provides hooks for data management. useTable manages pagination, sorting, and filtering:
// pages/users/list.tsx
import { useTable } from '@refinedev/antd';
import { Table, Space, Button, Input } from 'antd';
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { useDeleteMany } from '@refinedev/core';
export function UserList() {
const { tableProps, searchFormProps } = useTable({
resource: 'users',
sorters: { initial: [{ field: 'createdAt', order: 'desc' }] },
filters: {
initial: [{ field: 'isActive', operator: 'eq', value: true }],
},
syncWithLocation: true,
});
const { mutate: deleteMany } = useDeleteMany();
return (
<Table
{...tableProps}
rowKey="id"
rowSelection={{
onChange: (selectedKeys) => {
// selectedKeys for bulk actions
},
}}
>
<Table.Column dataIndex="id" title="ID" sorter />
<Table.Column dataIndex="name" title="Name" sorter />
<Table.Column dataIndex="email" title="Email" />
<Table.Column
dataIndex="role"
title="Role"
filters={[
{ text: 'Administrator', value: 'admin' },
{ text: 'Editor', value: 'editor' },
]}
/>
<Table.Column
title="Actions"
render={(_, record) => (
<Space>
<Button icon={<EditOutlined />} href={`/users/edit/${record.id}`} />
<Button
danger
icon={<DeleteOutlined />}
onClick={() => deleteMany({ resource: 'users', ids: [record.id] })}
/>
</Space>
)}
/>
</Table>
);
}
Edit Form with useForm
// pages/users/edit.tsx
import { useForm, Edit } from '@refinedev/antd';
import { Form, Input, Select } from 'antd';
export function UserEdit() {
const { formProps, saveButtonProps, queryResult } = useForm({
resource: 'users',
action: 'edit',
redirect: 'list',
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
<Form.Item
name="name"
label="Name"
rules={[{ required: true, message: 'Enter name' }]}
>
<Input />
</Form.Item>
<Form.Item
name="email"
label="Email"
rules={[{ required: true, type: 'email' }]}
>
<Input />
</Form.Item>
<Form.Item name="role" label="Role">
<Select options={[
{ value: 'admin', label: 'Administrator' },
{ value: 'editor', label: 'Editor' },
{ value: 'user', label: 'User' },
]} />
</Form.Item>
</Form>
</Edit>
);
}
authProvider
import { AuthProvider } from '@refinedev/core';
export const authProvider: AuthProvider = {
login: async ({ email, password }) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
return { success: false, error: { message: 'Invalid credentials' } };
}
const { token, user } = await res.json();
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
return { success: true, redirectTo: '/' };
},
logout: async () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
return { success: true, redirectTo: '/login' };
},
check: async () => {
const token = localStorage.getItem('token');
if (token) return { authenticated: true };
return { authenticated: false, redirectTo: '/login' };
},
getPermissions: async () => {
const user = JSON.parse(localStorage.getItem('user') ?? '{}');
return user.role;
},
getIdentity: async () => {
const user = JSON.parse(localStorage.getItem('user') ?? '{}');
return { id: user.id, name: user.name, avatar: user.avatar };
},
onError: async (error) => {
if (error.status === 401) return { logout: true };
return {};
},
};
Access Control
Refine supports several permission providers: Casbin, Cerbos, custom:
import { AccessControlProvider } from '@refinedev/core';
export const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
const user = JSON.parse(localStorage.getItem('user') ?? '{}');
// admin — all allowed
if (user.role === 'admin') return { can: true };
// editor — read and edit only, no delete
if (user.role === 'editor') {
if (action === 'delete') return { can: false, reason: 'No permissions' };
return { can: true };
}
return { can: false };
},
};
<Refine accessControlProvider={accessControlProvider}>
Difference from React Admin
| Aspect | React Admin | Refine |
|---|---|---|
| UI dependency | Material UI | any (headless) |
| Routing | built-in | react-router / next.js |
| Learning curve | lower | slightly higher |
| UI flexibility | limited | full |
| TypeScript | partial | complete |
| SSR/Next.js | difficult | native |
Timeline
- MVP (3–5 resources, Ant Design, standard CRUD): 3–5 days
- Full panel (custom UI, complex forms, access control, file upload): 2–3 weeks
- Integration with Next.js App Router and SSR: another 3–5 days







