Cuando trabajas en un backoffice que consume APIs con respuestas JSON enormes —listados de miles de usuarios, pedidos, productos o registros de auditoría— tarde o temprano te enfrentas al mismo problema: el estado de tu aplicación se vuelve un desastre de arrays gigantes que hay que recorrer una y otra vez para encontrar, actualizar o eliminar un solo elemento. Aquí es donde entra el patrón Entity Adapter, popularizado en el ecosistema Angular a través de @ngrx/entity.
El problema que resuelve
Imagina que tu backend te devuelve un JSON con 5.000 empleados para una tabla de gestión de RRHH. La forma "naive" de manejar esto en el estado de tu aplicación es guardarlo tal cual, como un array:
interface State {
employees: Employee[];
}
El problema aparece en cuanto necesitas hacer operaciones básicas:
-
Buscar un empleado por ID → recorres el array completo con
find(). -
Actualizar un empleado → recorres el array con
map(), comparas IDs, y reconstruyes todo el array. -
Eliminar un empleado → recorres con
filter().
Cada una de estas operaciones es O(n). Con 5.000 registros no es catastrófico, pero si tu backoffice hace esto constantemente (filtros, ediciones en línea, actualizaciones en tiempo real vía WebSocket), el rendimiento se degrada y el código se llena de bucles repetidos por todas partes.
El Entity Adapter resuelve esto cambiando la estructura de datos de fondo: en lugar de un array, normaliza la colección en un diccionario indexado por ID más un array de IDs que define el orden:
interface EntityState<T> {
ids: string[] | number[];
entities: { [id: string]: T };
}
Buscar, actualizar o eliminar por ID pasa de O(n) a O(1), porque accedes directo por clave en el diccionario entities, sin recorrer nada.
Por qué conviene en una arquitectura BackOffice con JSON grandes
En un backoffice típico conviven varias colecciones grandes al mismo tiempo (usuarios, pedidos, productos, logs) y todas necesitan las mismas operaciones CRUD sobre el estado. El Entity Adapter aporta:
-
Normalización consistente: todas tus colecciones siguen la misma forma (
ids+entities), así que el código de acceso a datos se vuelve predecible y reutilizable entre features distintas. -
Métodos ya resueltos:
addOne,updateOne,removeOne,upsertMany,setAll... el adapter te da funciones puras y probadas para las operaciones comunes, evitando que cada desarrollador del equipo reinvente su propio reducer con bucles manuales. -
Selectores generados automáticamente:
selectAll,selectEntities,selectIds,selectTotal— te ahorras escribir selectores desde cero para cada entidad. - Rendimiento en actualizaciones parciales: si llega un WebSocket avisando que el pedido #4521 cambió de estado, actualizas solo esa entrada del diccionario sin tocar las otras 4.999.
Ventajas
- Complejidad O(1) en operaciones por ID, crítico cuando manejas miles de registros.
-
Menos código repetitivo: los reducers dejan de estar llenos de
.map()y.filter()anidados. - Predictibilidad: al normalizar, evitas datos duplicados o desincronizados (por ejemplo, el mismo empleado apareciendo con datos distintos en dos partes del estado).
- Escalabilidad del equipo: cualquier desarrollador que conozca el patrón entiende de inmediato cómo está estructurada cualquier colección del proyecto.
- Integración natural con NgRx: si ya usas Store y Effects, el adapter encaja directo en tus reducers sin fricción.
Desventajas
- Curva de aprendizaje inicial: si el equipo no conoce NgRx Entity, hay que invertir tiempo en entender la normalización y los selectores generados.
- Overhead para colecciones pequeñas: si tu lista tiene 20 elementos, montar todo el aparato de adapter es matar moscas a cañonazos; un array simple basta.
- Relaciones anidadas complejas: cuando las entidades tienen relaciones profundas entre sí (un pedido con líneas de pedido, que a su vez tienen productos), normalizar todo correctamente requiere diseño cuidadoso, no es automático.
- Dependencia del ecosistema NgRx: si tu proyecto no usa NgRx, tendrías que adaptar el concepto manualmente o evaluar otras librerías.
Implementación agnóstica: interfaces y bucle de lectura
El patrón no depende de ningún framework en concreto, es simplemente una estructura de datos normalizada. Lo primero es definir el tipo de estado normalizado:
export interface Employee {
id: number;
name: string;
department: string;
active: boolean;
}
interface EntityState<T> {
ids: number[];
entities: Record<number, T>;
}
function createInitialState<T>(): EntityState<T> {
return { ids: [], entities: {} };
}
Luego se escriben las funciones que manipulan ese estado: setAll, updateOne y removeOne. El "bucle de lectura" —cómo se transforma el JSON gigante que llega del backend en el diccionario normalizado— vive dentro de setAll, típicamente con un reduce que recorre el array una sola vez:
function setAll<T extends { id: number }>(
items: T[],
state: EntityState<T>
): EntityState<T> {
const entities = items.reduce<Record<number, T>>((acc, item) => {
acc[item.id] = item;
return acc;
}, {});
return {
ids: items.map((item) => item.id),
entities,
};
}
function updateOne<T extends { id: number }>(
id: number,
changes: Partial<T>,
state: EntityState<T>
): EntityState<T> {
const existing = state.entities[id];
if (!existing) return state;
return {
...state,
entities: {
...state.entities,
[id]: { ...existing, ...changes },
},
};
}
function removeOne<T>(id: number, state: EntityState<T>): EntityState<T> {
const { [id]: removed, ...rest } = state.entities;
return {
ids: state.ids.filter((existingId) => existingId !== id),
entities: rest,
};
}
Con estas tres funciones puras ya tienes el patrón completo, sin depender de ninguna librería externa. A partir de aquí, cada framework lo integra en su propio mecanismo de estado.
¿Hay que implementarlo siempre a mano?
No necesariamente. Al ser un patrón tan común, algunos ecosistemas ya traen una implementación lista para usar. En Angular, por ejemplo, el paquete @ngrx/entity (un complemento opcional dentro del ecosistema NgRx, no la misma cosa) provee createEntityAdapter con las funciones setAll, updateOne, removeOne y selectores ya generados, ahorrándote escribir el código de la sección anterior. En React no hay un paquete tan estandarizado, pero Redux Toolkit incluye su propio createEntityAdapter con el mismo enfoque; sin librería, el patrón se integra igual de bien dentro de un useReducer, reutilizando exactamente las mismas funciones puras.
Da igual si el proyecto usa Angular, React o cualquier otro framework: la idea de fondo es la misma en todos, dejar de tratar tus colecciones como arrays y empezar a tratarlas como diccionarios.
Conclusión
El Entity Adapter no es magia, es simplemente aplicar normalización de datos —un principio bien conocido en bases de datos relacionales— al estado del frontend. En un backoffice donde constantemente llegan JSON grandes y se hacen operaciones CRUD sobre colecciones extensas, este patrón evita que el código se llene de bucles repetidos y mejora sensiblemente el rendimiento en las operaciones más comunes: buscar, actualizar y eliminar por ID.
Top comments (0)