Розробка адміністративної панелі на Refine
Refine — це headless React-фреймворк для адміністративних інтерфейсів. На відміну від React Admin, який поставляє компоненти на базі Material UI, Refine розділяє логіку та UI: ядро керує станом, маршрутизацією, завантаженням даних, правами — а розробник обирає 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 {};
},
};
Контроль доступу
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 | складно | natively |
Строки виконання
- MVP (3–5 ресурсів, Ant Design, стандартний CRUD): 3–5 днів
- Повноцінна панель (користувацький UI, складні форми, контроль доступу, завантаження файлів): 2–3 тижні
- Інтеграція з Next.js App Router і SSR: ще 3–5 днів







