Estado compartido entre features: dónde vive lo de todos
Cuando dos funcionalidades necesitan el mismo dato, la salida perezosa es promoverlo a una carpeta común, y repetida cien veces esa salida produce el slice omnisciente del que depende toda la aplicación. Esta lección propone un procedimiento de tres pruebas —propietario natural, clase de estado y número real de consumidores— para decidir si un dato compartido se queda en su dominio, sube a una capa compartida o simplemente no pertenece al store, y trata la promoción como un movimiento reversible con criterios explícitos de subida y de bajada en lugar de como un destino permanente.
El corte por dominio funciona mientras cada dato tenga un dueño evidente, y se tensa el día que dos funcionalidades necesitan lo mismo. La reacción instintiva —mover el dato a una carpeta común para que ambas lo alcancen— parece neutral y es la que, aplicada sin criterio y repetida durante un año, produce el estado del que todos dependen y nadie mantiene: un slice compartido que nació con la sesión del usuario y acabó guardando preferencias, banderas, contadores y tres cachés de listas. El problema de fondo es que compartir se percibe como un acto sin coste, cuando en realidad es una promoción de alcance, y todo aumento de alcance amplía la superficie de cambio de algo que ya funcionaba. La disciplina consiste en tener un procedimiento para decidirlo, y en aceptar que la respuesta correcta muchas veces es no mover nada.
- Aplicar la prueba del propietario natural antes de considerar cualquier promoción de un dato compartido.
- Descartar del store lo que en realidad pertenece a la caché de servidor, a la URL o al estado local.
- Distinguir el estado compartido legítimo, sin dueño posible, del cajón de sastre que lo imita.
- Tratar la promoción y la degradación como movimientos reversibles con criterios explícitos.
La prueba del propietario natural
Antes de mover nada, hay una pregunta que resuelve la mayoría de los casos: ¿existe un dominio para el que este dato sea constitutivo, es decir, uno que no podría hacer su trabajo sin él mientras los demás solo lo consultan? Si la respuesta es sí, el dato se queda donde está y los demás lo leen por su selector público. Compartir no exige mudarse: exige publicar.
Hay un segundo criterio que afina el primero cuando la titularidad no es evidente: mirar quién escribe en lugar de quién lee. Los lectores de un dato pueden ser muchos sin que eso diga nada sobre su dueño, pero los escritores casi siempre son pocos, y el dominio que lo escribe es el que define sus invariantes, sus transiciones válidas y su ciclo de vida. Un dato con un único escritor tiene dueño aunque lo lea media aplicación; un dato con escritores en tres dominios distintos es un candidato serio a transversal, o una señal de que el modelo está mal cortado.
La confusión nace de identificar ser usado por varios con pertenecer a varios. El catálogo de productos lo consultan el buscador, el carrito y las recomendaciones, y sin embargo solo hay un dominio que define qué es un producto, decide cuándo se recarga y conoce su forma; ese es su propietario, y los otros tres son lectores. Un dato tiene tantos lectores como haga falta sin que su titularidad se diluya, exactamente igual que una tabla de una base de datos no cambia de dueño porque muchas consultas la lean.
// Reflejo perezoso: el dato sube a la capa comun porque lo miran dos
const comun = createSlice({
name: 'comun',
initialState: { catalogo: [] as Producto[] },
reducers: {},
})
// Disciplina: se queda con su dueno y publica una lectura
const catalogo = createSlice({
name: 'catalogo',
initialState: { items: [] as Producto[] },
reducers: {},
selectors: { selectItems: (s) => s.items },
})
export const { selectItems } = catalogo.selectors // lo usan buscador y carrito
Ante la duda, el movimiento barato es exportar un selector desde el dominio propietario y dejar el estado donde está. Si más adelante resulta que el propietario no era tal, promover el dato entonces cuesta lo mismo que costaba hoy, con la ventaja de que ya sabrás quién lo consume de verdad. La secuencia inversa —promover primero y descubrir después que había dueño— es la que no se deshace, porque en cuanto un dato vive en la capa común todo el mundo se acostumbra a alcanzarlo desde cualquier sitio.
Lo que no debería estar en el store
Una porción sustancial de lo que los equipos promueven a una capa compartida no es estado compartido: es otra cosa mal clasificada. El caso dominante es la caché de servidor. Dos funcionalidades que necesitan la misma lista de pedidos no tienen un problema de estado compartido, tienen dos consumidores del mismo recurso remoto, y la respuesta es una consulta con la misma clave en RTK Query o en TanStack Query, donde la deduplicación y la invalidación ya están resueltas. Promover esa lista a un slice común reintroduce a mano un problema que la capa de caché resuelve de fábrica.
El segundo caso es el estado navegable. Filtros, pestaña activa, término de búsqueda y paginación se comparten entre componentes con naturalidad porque su sitio es la URL, no un slice, y allí ganan además persistencia al recargar y compartibilidad por enlace. El tercero es el falso compartido: el dato que solo necesitan dos componentes que resultan estar en ramas distintas del árbol, y que se resuelve elevando el estado al ancestro común o pasándolo por contexto, sin tocar el store en absoluto.
flowchart TD
A[dos features necesitan el mismo dato] --> B{tiene propietario natural?}
B -->|si| C[se queda y exporta selector]
B -->|no| D{de donde viene la verdad?}
D -->|del servidor| E[cache con RTK Query]
D -->|es navegable| F[la URL]
D -->|del cliente| G{cuantas features lo escriben?}
G -->|una escribe y otras leen| C
G -->|varias escriben| H[slice compartido con dueno declarado]
style C fill:#a6e3a1,color:#11111b
style E fill:#f38ba8,color:#11111b
style F fill:#fab387,color:#11111b
style H fill:#cba6f7,color:#11111bEstos tres desvíos tienen algo en común que explica por qué se confunden tan a menudo con estado compartido: en los tres, el dato lo necesitan varios sitios. Compartir el uso es la única propiedad que exhiben, y es justamente la que no basta para decidir nada. La pregunta que discrimina no es cuántos lo usan sino de dónde viene su verdad, que es el mismo criterio con el que el árbol de decisión del Nivel 7 clasificaba cualquier pieza de estado antes de asignarle herramienta.
Filtrado por estas tres preguntas, el conjunto de candidatos a estado compartido genuino se reduce drásticamente, y lo que queda tiene un aire de familia reconocible: sesión y permisos, idioma y tema, notificaciones globales, banderas de funcionalidad. Son datos transversales por naturaleza, que ningún dominio de producto podría reclamar sin que sonara arbitrario, y que suelen escribirse en pocos sitios y leerse en muchos.
Con dueño: se queda
Un dominio lo define y otros lo consultan. Se comparte publicando un selector, no mudando el estado.
Del servidor: a la caché
Misma clave, misma consulta. La deduplicación y la invalidación ya están resueltas fuera del store.
Navegable: a la URL
Filtros y pestañas se comparten por enlace y sobreviven a la recarga sin ocupar un slice.
Sin dueño: capa común
Sesión, idioma, permisos, banderas. Transversales de verdad, con un equipo responsable declarado.
Cómo evitar que la capa común se vuelva global
La capa común es, además, el sitio donde la disciplina de las fronteras se relaja primero, porque un slice sin dueño no tiene a nadie que defienda su superficie pública. Si los transversales exponen su estado en crudo y cualquier funcionalidad lo alcanza por su forma interna, has creado la peor combinación posible: alcance global y ninguna frontera. Un transversal merece exactamente las mismas reglas que un dominio de producto, y quizá más, porque su radio de impacto es mayor.
Que exista una capa compartida legítima no impide que degenere, y degenera siempre por el mismo mecanismo: la ausencia de un criterio de entrada. Un directorio llamado shared o common sin reglas es una invitación abierta, porque cualquiera que dude sobre dónde poner algo encuentra allí un destino que nadie va a discutir en la revisión. La contramedida no es prohibirlo sino darle nombre propio y responsable: en vez de una carpeta genérica, un slice de sesion, otro de preferencias, otro de notificaciones, cada uno con la misma disciplina de superficie pública que cualquier dominio y con un equipo que responde por él.
// Mal: un cajon sin dueno que crece por acumulacion
const comun = createSlice({
name: 'comun',
initialState: { usuario: null, tema: 'oscuro', ultimoPedidoId: null, abierto: false },
reducers: { set: (s, a) => ({ ...s, ...a.payload }) },
})
// Bien: transversales con nombre, cada uno con su razon de cambio
const sesion = createSlice({ name: 'sesion', initialState: { usuario: null }, reducers: {} })
const preferencias = createSlice({ name: 'preferencias', initialState: { tema: 'oscuro' }, reducers: {} })
La forma más eficaz de mantener honesta esa capa es exigir un criterio de entrada escrito y aplicarlo en la revisión: un dato solo entra si no tiene propietario natural, si lo escriben varias funcionalidades y si su verdad está en el cliente. Tres condiciones que se comprueban en un minuto y que rechazan la inmensa mayoría de los candidatos. Sin ese filtro explícito, la decisión queda a merced de la prisa, y la prisa siempre vota por promover.
Una prueba barata para detectar el deterioro consiste en pedir, para cada slice compartido, una frase que empiece por «este estado existe porque». Si la frase sale nítida —«porque la identidad del usuario la escriben el inicio y el cierre de sesión, y la leen todas las pantallas»—, el transversal está bien fundado. Si hace falta enumerar tres casos distintos unidos por la conjunción «y también», el slice está mezclando conceptos y pide una división. Es la misma disciplina que se aplica a los mensajes de confirmación de cambios y funciona por la misma razón: obligar a verbalizar el motivo revela cuándo no hay ninguno.
La diferencia entre ambos bloques no es de tamaño sino de razón de cambio. El primero mezcla cuatro conceptos que cambian por motivos distintos, así que cualquier modificación obliga a razonar sobre los otros tres y todo el mundo acaba dependiendo de todo. El segundo mantiene la propiedad que hace útil el corte por dominio: un cambio en el tema no roza la sesión. Un buen indicador de deterioro es el reducer genérico del tipo set que acepta cualquier fragmento: cuando aparece, el slice ha dejado de modelar un dominio y se ha convertido en un almacén de claves.
Un inventario recorrido
Un ejemplo concreto muestra cuánto filtra el procedimiento. Toma una tienda con cinco datos que hoy viven en la carpeta común: el usuario autenticado, el catálogo de productos, el término de búsqueda, el contador de artículos del carrito y la lista de pedidos recientes que aparece en dos pantallas. Parecen cinco casos de estado compartido y, aplicadas las tres pruebas, solo uno lo es.
El usuario autenticado no tiene propietario posible —ningún dominio de producto puede reclamar la sesión— y lo escriben el inicio de sesión y el cierre: es un transversal genuino y se queda, con nombre estrecho y responsable. El catálogo sí tiene dueño, así que vuelve a su dominio y publica un selector. El término de búsqueda es navegable y se va a la URL, donde además gana compartibilidad. Los pedidos recientes vienen del servidor y se resuelven con una consulta bajo la misma clave desde las dos pantallas. Y el contador del carrito no es estado en absoluto: es una derivación de las líneas, que se computa con un selector y no se guarda en ningún sitio.
El recorrido enseña de paso que las tres pruebas no compiten entre sí sino que se aplican en orden, y que el orden importa: preguntar primero por el propietario evita discusiones sobre dónde colocar algo que no había que mover, y preguntar después por el origen de la verdad saca del store todo lo que nunca debió entrar. Solo el residuo de ambas preguntas merece la tercera, la del número real de escritores, que es la que finalmente decide entre dominio y transversal.
De cinco candidatos queda uno, y esa proporción es habitual cuando el procedimiento se aplica en serio. La consecuencia práctica importa más que el recuento: cada dato devuelto a su sitio reduce el número de funcionalidades que tienen que coordinarse para cambiarlo, y el único que permanece en la capa común lo hace por una razón que se puede escribir en una línea y defender en una revisión.
Promover un dato a la capa común cuesta cinco minutos y ahorra una importación; parece una ganancia. Lo que cambia de verdad es el radio de impacto de cualquier modificación futura sobre él, que pasa de una carpeta a toda la aplicación. A partir de ahí, cada cambio en su forma exige revisar consumidores que no conoces, cada prueba de una funcionalidad arrastra el estado de otras, y nadie se atreve a borrar campos porque no hay forma de saber quién los lee. Es un préstamo con intereses diferidos: el beneficio es inmediato y visible, el coste llega repartido en cada revisión de los meses siguientes.
Hay además una asimetría de esfuerzo que conviene tener presente al planificar: promover es una operación mecánica que cualquiera puede hacer en una tarde, mientras que devolver un dato a su dominio exige entender a todos sus consumidores actuales. Esa asimetría explica por qué las capas comunes crecen de forma monótona en casi todos los productos: no es que nadie quiera limpiarlas, es que subir cuesta minutos y bajar cuesta días. La única contramedida realista es no dejar que suba lo que no debe.
La promoción se presenta casi siempre como un viaje de ida, y no tiene por qué serlo. Si un dato subió a la capa común y hoy solo lo consume una funcionalidad —porque las otras desaparecieron o cambiaron de enfoque—, bajarlo a ese dominio es una mejora estricta: reduce alcance, reduce revisión y devuelve la libertad de cambiarle la forma. Revisar periódicamente el número real de consumidores de cada transversal, con una búsqueda que cuente sus selectores, es una de las tareas de higiene con mejor retorno en un store grande, precisamente porque nadie la hace nunca.
El vocabulario con el que hablamos de esto oculta lo que ocurre. Decimos que un dato se comparte, como si la operación fuera aditiva e inocua, cuando lo que realmente sucede es que su propietario pierde la potestad de cambiarlo unilateralmente. Un dato con dueño se puede renombrar, normalizar, dividir o borrar en una tarde, porque el conjunto de afectados cabe en una carpeta; el mismo dato promovido a compartido se vuelve una interfaz pública de facto, y toda modificación pasa a requerir la coordinación de partes que ni siquiera conoces. Ese es el coste real, y no aparece en el momento de compartir sino difuminado en cada cambio posterior, que es exactamente lo que lo hace tan fácil de subestimar. La lectura correcta invierte la pregunta habitual: no se trata de decidir quién puede acceder a un dato —en un store global todo el mundo puede, esa batalla está perdida de antemano— sino de decidir quién conserva la autoridad para cambiarlo. Compartir sin dueño declarado es declarar que nadie la tiene, y un dato sobre el que nadie tiene autoridad no se mantiene: se congela, porque cualquier cambio es demasiado arriesgado, y se congela justo en la forma que tenía el día en que alguien tuvo prisa. Por eso el estado transversal sano no es el que menos gente usa, sino el que tiene un nombre estrecho, una razón de cambio única y un responsable con potestad para decir que no. La escala de una aplicación no la mide el tamaño de su store, la mide cuántas de sus piezas siguen teniendo alguien que pueda cambiarlas sin pedir permiso a todos.
- Lista todo lo que hoy vive en tu slice o carpeta compartida y, para cada elemento, cuenta cuántas funcionalidades lo leen y cuántas lo escriben.
- Aplica la prueba del propietario natural a cada uno. Devuelve a su dominio todo lo que tenga un dueño evidente y publica un selector en su lugar.
- Marca lo que en realidad es caché de servidor y muévelo a una consulta con clave compartida; marca lo navegable y muévelo a la URL.
- De lo que quede, comprueba si es un transversal genuino y dale nombre estrecho: un slice por concepto, nunca un cajón con un reducer genérico.
- Asigna a cada transversal un equipo responsable y escribe en una línea su razón de cambio. Si no sabes escribirla, el slice mezcla conceptos.
- Busca candidatos a degradación: transversales con un único consumidor real. Bájalos a ese dominio y comprueba qué se simplifica.