Реалізація Presence (online-статус, активність) для collaboration в мобільному додатку
Presence — це ephemeral шар даних про те, що користувач робить прямо зараз: online ли він, на якому екрані, що редагує, де його курсор. На відміну від звичайних даних додатка, presence не потрібно зберігати у БД — воно живе тільки поки активно з'єднання. Але реалізувати його правильно складніше, ніж кажется.
Чому не можна просто зберігати is_online у Firestore
Класична помилка: додати поле lastSeen у документ користувача та оновлювати його кожні 30 секунд. Проблеми:
- Убийство батареї: постійні write-операції у фоні. Android 8+ обмежує фонові задачі, iOS убиває Background Fetch через кілька хвилин.
-
Некоректний статус при крешу: додаток упав —
is_online: trueзалишиться до наступного обновлення. - Гонки при кількох пристроях: користувач online на телефоні і планшеті. Вийшов з планшета — обнулив статус, хоча телефон ще активний.
Firebase Realtime Database: правильна реалізація через onDisconnect
Firebase RTDB має вбудований механізм — onDisconnect(). Сервер виконує задану операцію автоматично при розриві з'єднання, навіть якщо клієнт просто втратив мережу:
import database from '@react-native-firebase/database';
const userStatusRef = database().ref(`/status/${userId}`);
const isOfflineData = { state: 'offline', lastChanged: database.ServerValue.TIMESTAMP };
const isOnlineData = { state: 'online', lastChanged: database.ServerValue.TIMESTAMP };
// Реєструємо дію на відключення ДО встановлення online
await userStatusRef.onDisconnect().set(isOfflineData);
await userStatusRef.set(isOnlineData);
database.ServerValue.TIMESTAMP — серверна мітка часу, не залежить від часового поясу пристрою. Важливо: onDisconnect реєструється до set(isOnlineData) — інакше race condition, при якому клієнт може відключитися між двома викликами.
Для підтримки кількох пристроїв — лічильник активних сесій замість булевого флага:
// Використовуємо транзакцію для атомарного increment
const sessionsRef = database().ref(`/sessions/${userId}`);
await sessionsRef.transaction(current => (current || 0) + 1);
await sessionsRef.onDisconnect().transaction(current => Math.max((current || 1) - 1, 0));
is_online = sessions > 0. Крах на одному пристрої зменшить лічильник через onDisconnect, не торкнеться інших сесій.
AppState: синхронізація з життєвим циклом iOS/Android
import { AppState, AppStateStatus } from 'react-native';
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextState: AppStateStatus) => {
if (nextState === 'active') {
userStatusRef.onDisconnect().set(isOfflineData);
userStatusRef.set(isOnlineData);
} else if (nextState === 'background' || nextState === 'inactive') {
userStatusRef.set(isOfflineData);
userStatusRef.onDisconnect().cancel(); // скасовуємо, вже зробили вручну
}
});
return () => subscription.remove();
}, []);
На Android при background у вас є кілька секунд до того, як система заморозить JS-тред. userStatusRef.set() — асинхронна операція, не гарантована. onDisconnect() як fallback обов'язковий.
Типизований presence з додатковим контекстом
Окрім online/offline, часто потрібно знати, що саме робить користувач:
type PresenceState = {
status: 'online' | 'idle' | 'offline';
currentScreen: string | null;
editingItemId: string | null;
lastChanged: number;
};
idle — користувач відкрив додаток, але 5+ хвилин не дотикався екрана. Детектується через PanResponder або TouchableWithoutFeedback на кореневому компоненті з debounce-таймером.
Отображення: аватари з індикатором
До списку учасників з presence-даними додаємо кольоровий бейдж:
- Зелений:
status === 'online' - Жовтий:
status === 'idle' - Сірий:
status === 'offline', показуємоlastChangedяк «був online N хвилин тому»
Нюанс: не оновлюйте lastChanged при кожній зміні presence — тільки при зміні status. Інакше список буде перерендериватися кожні кілька секунд для кожного активного користувача.
Оцінка
Firebase RTDB presence з підтримкою кількох пристроїв, idle-detection та UI-компонентом: 1–3 тижні. Custom WebSocket presence на власному бекенді: 2–4 тижні.







