Разработка административной панели на Refine
Refine — headless React-фреймворк для admin-интерфейсов. В отличие от React Admin, который поставляет компоненты на базе Material UI, Refine разделяет логику и UI: ядро управляет состоянием, маршрутизацией, data fetching, правами — а UI-компоненты выбирает разработчик (Ant Design, Material UI, Chakra, Mantine, или полностью кастомные).
Установка
npm create refine-app@latest -- --preset refine-nextjs
# или для Vite + Ant Design:
npm create refine-app@latest -- --preset refine-vite
Ручная установка для существующего проекта:
npm install @refinedev/core @refinedev/react-router-v6
# UI пакет на выбор:
npm install @refinedev/antd antd
# или:
npm install @refinedev/mui @mui/material @emotion/react
Структура проекта
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: 'Пользователи', icon: <UserOutlined /> },
},
{
name: 'products',
list: '/products',
edit: '/products/edit/:id',
meta: { label: 'Товары' },
},
]}
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 использует стандартизированный интерфейс dataProvider:
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,
};
Список с useTable
Refine предоставляет хуки для работы с данными. useTable управляет пагинацией, сортировкой и фильтрацией:
// 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 для массовых действий
},
}}
>
<Table.Column dataIndex="id" title="ID" sorter />
<Table.Column dataIndex="name" title="Имя" sorter />
<Table.Column dataIndex="email" title="Email" />
<Table.Column
dataIndex="role"
title="Роль"
filters={[
{ text: 'Администратор', value: 'admin' },
{ text: 'Редактор', value: 'editor' },
]}
/>
<Table.Column
title="Действия"
render={(_, record) => (
<Space>
<Button icon={<EditOutlined />} href={`/users/edit/${record.id}`} />
<Button
danger
icon={<DeleteOutlined />}
onClick={() => deleteMany({ resource: 'users', ids: [record.id] })}
/>
</Space>
)}
/>
</Table>
);
}
Форма редактирования с 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="Имя"
rules={[{ required: true, message: 'Введите имя' }]}
>
<Input />
</Form.Item>
<Form.Item
name="email"
label="Email"
rules={[{ required: true, type: 'email' }]}
>
<Input />
</Form.Item>
<Form.Item name="role" label="Роль">
<Select options={[
{ value: 'admin', label: 'Администратор' },
{ value: 'editor', label: 'Редактор' },
{ value: 'user', label: 'Пользователь' },
]} />
</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: 'Неверные учётные данные' } };
}
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 поддерживает несколько провайдеров прав: Casbin, Cerbos, кастомный:
import { AccessControlProvider } from '@refinedev/core';
export const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
const user = JSON.parse(localStorage.getItem('user') ?? '{}');
// admin — всё разрешено
if (user.role === 'admin') return { can: true };
// editor — только чтение и редактирование, без удаления
if (user.role === 'editor') {
if (action === 'delete') return { can: false, reason: 'Нет прав' };
return { can: true };
}
return { can: false };
},
};
<Refine accessControlProvider={accessControlProvider}>
Отличие от React Admin
| Аспект | React Admin | Refine |
|---|---|---|
| UI зависимость | Material UI | любой (headless) |
| Роутинг | встроенный | react-router / next.js |
| Кривая обучения | ниже | чуть выше |
| Гибкость UI | ограничена | полная |
| TypeScript | частично | полностью |
| SSR/Next.js | сложно | нативно |
Сроки
- MVP (3–5 ресурсов, Ant Design, стандартные CRUD): 3–5 дней
- Полноценная панель (кастомный UI, сложные формы, access control, загрузка файлов): 2–3 недели
- Интеграция с Next.js App Router и SSR: ещё 3–5 дней







