Пару месяцев назад я взялся за проект — real-time POS для ресторана, с учетом кухни, официантов и курьеров. На стек взял Next.js + TypeScript + Tailwind, а для мгновенной синхронизации — Pusher. Казалось бы, классика real-time, но когда зашли в режим rush hour с кучей одновременных обновлений, столкнулся с парой важных нюансов, которые часто упускают из виду.
Расскажу, как я решал три главные проблемы: синхронизацию состояния со стороны фронта, логи отклонений заказов (rejection logs) и UX для максимально быстрой работы с клавиатуры.
Синхронизация состояния и отклонения заказов: не всё так просто
Идея была стандартной: когда на кухне заканчивается ингредиент, шеф может reject заказ с объяснением — например, “Out of stock”. Это мгновенно должно отразиться у официантов, админов и в истории заказов.
Pusher отлично справляется с пушами, но real-time — это не всегда real-ideal. В rush hour с десятками параллельных обновлений я столкнулся с двумя главными болью:
- Расхождение состояния из-за сетевых задержек. Пуш-сообщения могут прийти с задержкой или в неправильном порядке, особенно если клиент пересобирает состояние после reconnect. В итоге UI показывал “принято”, когда заказ уже отменён.
- Роллбэки UI и откат изменений. Если официант меняет статус заказа локально, а сервер отвергает — как синхронизировать это без мерцаний и конфликтов?
Решение у меня получилось такое:
-
Версионирование статусов: каждый заказ хранит поле
versionилиupdatedAt. При получении пуша с обновлением, фронт проверяет, не старее ли пришедшие данные текущего состояния. Если да — игнорирует, если нет — апдейтит. -
Оптимистичные обновления с откатом: когда официант меняет статус, UI сразу меняет состояние (optimistic update) и запускает запрос на сервер. Если сервер в ответе говорит “reject”, фронт откатывает изменения и показывает уведомление.
-
Логирование отклонений: все rejection-сообщения не только пушатся всем участникам, но и пишутся в отдельный лог с таймстампами. Это помогает администратору разобраться с проблемами и служит audit trail.
В итоге интерфейс стал максимально отзывчивым, без тупых задержек и несостыковок. Но это потребовало дополнительной логики и хорошего контроля версий данных.
Клавиатурные хоткеи и минимизация кликов — life hack для официанта
В ресторане каждая секунда на счету. Переключение между мышкой и клавиатурой — потеря времени и ритма. Поэтому я решил сделать интерфейс, полностью управляемый с клавиатуры.
Вот что помогло:
-
use-hotkeys + кастомные хуки: библиотека
use-hotkeysотлично работает в Next.js/React и легко настраивается. Я сделал отдельный хук, который слушает нужные комбинации и переключает фокус между полями или вызывает действие. -
Tailwind для компактных и понятных UI: использовал утилиты Tailwind чтобы сверстать интерфейс с минимальным количеством tap targets, все максимально large и логично расположено.
-
Фокус на табы и enter: табом переходишь между основными полями, enter — подтверждение. Быстрый доступ к популярным действиям через
Ctrl + цифраилиAlt + буква. -
Дебаунс на ввод: чтобы не дергать сервер на каждый нажатый символ, реализовал дебаунс 200мс перед отправкой поисковых запросов или обновлением состояния.
На практике это сильно ускоряет работу официантов. У меня был скепсис насчет сложности обучения, но после пары дней все вошло в привычку.
Пример: обработка rejection с версиями и откатом
type OrderStatus = "pending" | "accepted" | "rejected";
interface Order {
id: string;
status: OrderStatus;
version: number;
rejectionReason?: string;
}
const [order, setOrder] = React.useState<Order>(initialOrder);
async function updateOrderStatus(newStatus: OrderStatus, reason?: string) {
// Оптимистично обновляем UI
const optimisticVersion = order.version + 1;
setOrder({ ...order, status: newStatus, version: optimisticVersion, rejectionReason: reason });
try {
const res = await fetch(`/api/orders/${order.id}/status`, {
method: "POST",
body: JSON.stringify({ status: newStatus, reason }),
});
const data = await res.json();
if (data.version < optimisticVersion) {
// Сервер откатил изменения
setOrder(prev => prev); // возвращаем состояние, можно дополнительно показать ошибку
alert("Статус не обновился: " + data.message);
} else {
// Подтверждаем версию с сервера
setOrder(data.order);
}
} catch (e) {
// Ошибка сети, откатываем
setOrder(prev => prev);
alert("Ошибка обновления статуса");
}
}
// При пуше от Pusher:
function onPusherUpdate(updatedOrder: Order) {
if (updatedOrder.version > order.version) {
setOrder(updatedOrder);
}
}
Кому это зайдёт, а кому нет?
Если вы строите real-time дашборд с кучей параллельных апдейтов, вам точно пригодится паттерн с версиями и оптимистичными обновлениями. Pusher и аналоги — удобны, но на фронте нужно держать логику для согласованности данных.
Если вы делаете интерфейс для людей, которым важно скорость ввода — keyboard-first UX и хоткеи — must-have. Но будьте готовы вложиться в обучение и тестирование, чтобы не усложнить жизнь пользователям.
В целом, на практике такой подход позволяет сделать POS-систему, которая выдерживает реальные нагрузки и не раздражает персонал. Следующий шаг — добавить agentic workflows для автоматических действий на повторяющиеся сценарии, но это уже тема для отдельного разговора.
Если интересно, могу поделиться исходниками и подробнее обсудить, как наладить state reconciliation в более сложных real-time системах.
Источник: https://www.reddit.com/r/nextjs/comments/1wlc65j/built_a_realtime_restaurant_pos_with_nextjs/