Зауважимо: коли в ERP-системі складський звіт зависає на 10 секунд при завантаженні 50 000 рядків, користувачі починають використовувати Excel. Розробка веб-інтерфейсу ERP — це не про React з коробки, а про тонке налаштування віртуалізації, керування станом та вирішення конфліктів доступу. У багатокористувацькому середовищі критична блокування записів при одночасному редагуванні. Без proper optimistic concurrency control користувачі перезаписують дані один одного. Типові болі: повільне завантаження таблиць, плутанина з правами доступу, втрата даних при offline. Ми вирішуємо їх за допомогою SPA, віртуалізації, оптимістичних оновлень та Permission Guard для ERP. Розробляємо інтерфейси від MVP з чотирма модулями до повноцінної системи на 15+ модулів. У цьому матеріалі розберемо ключові архітектурні рішення та практичні прийоми, які допоможуть уникнути типових помилок і побудувати масштабований інтерфейс.
Вибір стеку для ERP-інтерфейсу
Вибір архітектури та стеку — фундамент, від якого залежить продуктивність і вартість підтримки. Розглянемо два поширених підходи: SPA та SSR.
| Критерій | SPA | SSR |
|---|---|---|
| Продуктивність при інтенсивному UI | Висока | Низька (кожен клік — запит) |
| Персоналізація робочих місць | Гнучка | Обмежена |
| Офлайн-режим (PWA) | Підтримується | Неможливий |
| SEO (не критично для ERP) | Погане | Добре |
| Початкове завантаження | Повільніше (bundle) | Швидше (HTML) |
Для ERP вибір однозначний — SPA. Виняток — якщо потрібно SEO для публічних частин (наприклад, каталог продукції).
Frontend стек: React 18+ з Concurrent Features, TypeScript (strict), TanStack Table для таблиць, TanStack Query для даних, React Hook Form + Zod для форм, Zustand для UI-стану. Компонентна бібліотека — Radix UI + Tailwind (гнучкість) або Ant Design (швидкість).
Як працювати з великими таблицями?
Таблиця на 50 000 рядків — типове завдання для складського обліку або звітності. Без віртуалізації браузер зависає. Завдяки віртуалізації час рендерингу падає до 100 мс навіть при 100 000 рядків.
import {
useReactTable,
getCoreRowModel,
flexRender,
type ColumnDef,
} from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
interface VirtualizedTableProps<T> {
data: T[];
columns: ColumnDef<T>[];
rowHeight?: number;
}
export function VirtualizedTable<T>({
data,
columns,
rowHeight = 40,
}: VirtualizedTableProps<T>) {
const parentRef = useRef<HTMLDivElement>(null);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
const { rows } = table.getRowModel();
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => rowHeight,
overscan: 20,
});
const virtualItems = virtualizer.getVirtualItems();
const totalSize = virtualizer.getTotalSize();
return (
<div ref={parentRef} className="overflow-auto h-full">
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-white z-10 shadow-sm">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
style={{ width: header.getSize() }}
className="text-left px-3 py-2 text-xs font-semibold text-gray-600 border-b"
>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{virtualItems.length > 0 && (
<tr style={{ height: virtualItems[0].start }}>
<td colSpan={columns.length} />
</tr>
)}
{virtualItems.map(virtualRow => {
const row = rows[virtualRow.index];
return (
<tr
key={row.id}
className="hover:bg-gray-50 border-b border-gray-100"
style={{ height: rowHeight }}
>
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="px-3 py-2 text-sm">
{flexRender(cell.column.columnDef.header, cell.getContext())}
</td>
))}
</tr>
);
})}
{virtualItems.length > 0 && (
<tr style={{ height: totalSize - virtualItems[virtualItems.length - 1].end }}>
<td colSpan={columns.length} />
</tr>
)}
</tbody>
</table>
</div>
);
}
Як прискорити відгук інтерфейсу?
Оптимістичні оновлення — користувач змінює статус замовлення, інтерфейс реагує негайно, не чекаючи відповіді сервера. При помилці відбувається відкат. Це скорочує сприйнятий час відгуку на 200–500 мс.
const queryClient = useQueryClient();
const updateStatus = useMutation({
mutationFn: (data: { orderId: string; status: OrderStatus }) =>
api.patch(`/orders/${data.orderId}/status`, { status: data.status }),
onMutate: async ({ orderId, status }) => {
await queryClient.cancelQueries({ queryKey: ['orders', orderId] });
const prev = queryClient.getQueryData(['orders', orderId]);
queryClient.setQueryData(['orders', orderId], (old: Order) => ({
...old, status,
}));
return { prev };
},
onError: (_err, { orderId }, context) => {
queryClient.setQueryData(['orders', orderId], context?.prev);
toast.error('Не вдалося змінити статус');
},
onSettled: (_, __, { orderId }) => {
queryClient.invalidateQueries({ queryKey: ['orders', orderId] });
},
});
Вирішення конфліктів даних при багатокористувацькому доступі
Використовуємо стратегію last-write-wins з версіонуванням сутностей. Кожен запис містить поле version (integer), яке збільшується при зміні. Клієнт передає поточну версію, сервер перевіряє збіг. Якщо версія застаріла — повертає 409 Conflict, клієнт отримує актуальні дані та пропонує користувачеві вирішити конфлікт. Це реалізовано поверх TanStack Query через механізм optimistic updates + rollback.
Розмежування доступу до модулів з Permission Guard
Створюємо компонент Permission Guard, який перевіряє наявність дозволу у користувача. Якщо дозволу немає — можна показати заглушку або нічого. Ролі та дозволи зберігаються на сервері, клієнт отримує їх при аутентифікації.
type Permission = 'orders:read' | 'orders:write' | 'stock:read';
function PermissionGuard({ permission, children, fallback = null }: {
permission: Permission;
children: ReactNode;
fallback?: ReactNode;
}) {
const user = useUser();
if (user.permissions.includes(permission)) {
return <>{children}</>;
}
return <>{fallback}</>;
}
Продуктивність: додаткові оптимізації
- Code splitting по модулях — користувач складу не завантажує модуль HR, прискорюючи завантаження на 40%.
- Дебаунс для пошуку та фільтрів — відправляємо запит не частіше одного разу на 300 мс, знижуючи навантаження на сервер у 3 рази.
- Мемоізація важких обчислень — звіти з агрегацією в браузері через
useMemo.
Стежимо за показниками Core Web Vitals — ключовими метриками продуктивності, які безпосередньо впливають на UX. MDN Web Docs рекомендує орієнтуватися на LCP < 2.5 с, FID < 100 мс, CLS < 0.1. Використовуємо TanStack Query для керування серверним станом.
Процес роботи
Етапи розробки ERP-інтерфейсу:
| Етап | Тривалість | Результат |
|---|---|---|
| Аналітика | 2–4 тижні | Опис бізнес-процесів, специфікація модулів |
| Проектування | 2–4 тижні | Архітектура, прототипи ключових екранів |
| Реалізація | 4–6 тижнів на модуль | Працюючий функціонал з тестами |
| Тестування | 2–3 тижні | Звіт про навантажувальне тестування, виправлення багів |
| Деплой | 1–2 тижні | CI/CD, документація, навчання |
- Аналітика — вивчаємо бізнес-процеси, визначаємо модулі та пріоритети.
- Проектування — створюємо архітектуру, обираємо стек, прототипуємо ключові екрани.
- Реалізація — ітеративна розробка з демо кожні два тижні.
- Тестування — unit, integration, e2e тести, навантажувальне тестування.
- Деплой — налаштування CI/CD, міграція даних, навчання користувачів.
На кожному етапі готуємо документацію: технічне завдання, архітектурні рішення, API docs, інструкції користувача.
Строки
- MVP з чотирма-п'ятьма модулями: 6–8 місяців для команди з трьох-чотирьох розробників.
- Повноцінна система (15+ модулів): 1,5–2 роки при тій же команді.
Ітеративний підхід — запуск мінімального набору, зворотний зв'язок від користувачів, поступове розширення. Спроба зробити все одразу веде до провалу.
Що входить в роботу
- Технічне завдання та архітектурна документація.
- Розробка та налаштування CI/CD.
- Покриття коду тестами (unit, integration).
- Документація API та документація користувача.
- Навчання адміністраторів та користувачів.
- Гарантійна підтримка 6 місяців після запуску.
Зв'яжіться з нами для оцінки вашого проекту. Отримайте консультацію з архітектури та строків.







