← Все статьи

Post-mortem: как Firebase RTDB «ломает» массивы в React

Удаление записи в Firebase RTDB превратило items в объект — TypeError и белый экран. Нормализация данных на клиенте.

Содержание

Коротко

Автор собирал «личную ОС» с ИИ на своей инфраструктуре и синхронизировал дашборд логов через 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 — лотерея для фронтенда.

На практике

  1. Нормализуйте при каждой подписке на изменения — не полагайтесь на Array.isArray сразу после удаления или вставки в середину списка в RTDB.
  2. Если пришёл объект с числовыми ключами — соберите массив через сортировку ключей и map, либо через Object.values с фильтрацией пустых слотов; выберите вариант под вашу модель «дырявых» индексов.
  3. Вынесите проверку типа в одну функцию, например asList<T>(value: unknown): T[], и вызывайте её в единственной точке перед рендером списка.
  4. Для критичных коллекций рассмотрите Firestore с документами или схему «id → запись + отдельное поле порядка id» — дороже по модели данных, зато предсказуемее для UI.
  5. Добавьте интеграционный тест: записать [a, b, c], удалить b, прочитать снимок — UI не должен падать.

Итог

Один сценарий удаления в RTDB способен обрушить весь фронт из-за неявного приведения типов. Защита — нормализация на клиенте, а не надежда на то, что JSON-дерево «само» останется массивом. Подробный разбор автора — по ссылке в блоке источника ниже.