Содержание
Коротко
Автор собирал «личную ОС» с ИИ на своей инфраструктуре и синхронизировал дашборд логов через Firebase RTDB. После удаления одной строки сервер ответил 200 OK, а React показал белый экран: items.map is not a function. Данные пришли не массивом, а объектом — типичная ловушка RTDB, когда в «массиве» появляется дырка в индексах.
Что произошло
Стек простой: RTDB держит состояние в реальном времени, React рисует список записей. Пользователь нажал кнопку удаления в консоли Firebase — запись исчезла, интерфейс упал целиком, хотя с точки зрения бэкенда всё прошло штатно.
В консоли разработчика — TypeError: items.map is not a function. Компонент списка ждал Array, а из клиента Firebase пришёл обычный объект.
Firebase RTDB — это JSON-дерево без отдельного типа «массив». Последовательность [a, b, c] на проводе выглядит как объект с ключами "0", "1", "2". Пока индексы идут подряд без пропусков, клиент Firebase часто возвращает настоящий массив JavaScript. Удалили элемент из середины — между ключами осталась «дыра», и клиент перестал приводить структуру к Array. Методы вроде .map и .filter перестают работать; необработанная ошибка в корне списка валит всё дерево компонентов.
Отдельно обидно, что TypeScript в проекте мог спокойно типизировать поле как string[], хотя в рантайме прилетело { "0": "a", "2": "c" } без ключа "1". Статическая типизация не спасает от семантики хранилища.
Почему это важно
Любой продукт на связке RTDB + React, где в UI редактируют списки, рискует повторить тот же сценарий — это не частность одного экспериментального проекта. Консоль Firebase рисует данные «как таблицу/массив», а фактический формат остаётся объектом; визуальное совпадение с массивом создаёт ложное ощущение, что клиент всегда получит Array.
Облако может быть источником истины по содержанию, но не гарантирует форму на клиенте. Нужен явный слой нормализации на границе чтения: один раз привести данные к массиву — и только потом отдавать в компоненты. Без этого каждая мутация списка в RTDB — лотерея для фронтенда.
На практике
- Нормализуйте при каждой подписке на изменения — не полагайтесь на
Array.isArrayсразу после удаления или вставки в середину списка в RTDB. - Если пришёл объект с числовыми ключами — соберите массив через сортировку ключей и
map, либо черезObject.valuesс фильтрацией пустых слотов; выберите вариант под вашу модель «дырявых» индексов. - Вынесите проверку типа в одну функцию, например
asList<T>(value: unknown): T[], и вызывайте её в единственной точке перед рендером списка. - Для критичных коллекций рассмотрите Firestore с документами или схему «
id → запись+ отдельное поле порядка id» — дороже по модели данных, зато предсказуемее для UI. - Добавьте интеграционный тест: записать
[a, b, c], удалитьb, прочитать снимок — UI не должен падать.
Итог
Один сценарий удаления в RTDB способен обрушить весь фронт из-за неявного приведения типов. Защита — нормализация на клиенте, а не надежда на то, что JSON-дерево «само» останется массивом. Подробный разбор автора — по ссылке в блоке источника ниже.