Diseño por capas para React: capas, reglas de importación y cuándo detenerse
Aprenda cómo las capas de Feature-Sliced Design, la regla de importación unidireccional, las APIs públicas simplificadas y las importaciones cruzadas @x desenredan los conjuntos de código de React, y cuándo adoptar solo una parte de ellas.
Fuentes: Feature-Sliced Design in React: How to Structure a Scalable Codebase (and When It’s Overkill)
Casi todos los repositorios de código React que sobreviven más allá de su segundo año tienen una carpeta utils/ que nadie quiere abrir. Dentro se encuentran un formateador de fechas, un contenedor para intentos repetidos, media máquina de estados y un archivo helpers.ts que importa desde tres carpetas funcionales, mientras que dos de esas carpetas lo importan a él, por lo que nadie puede determinar qué depende de qué. El Diseño por Capas Funcionales (FSD) es una metodología dirigida específicamente a ese problema. Después de leer esto, conocerá sus seis capas, la única regla de importación que realiza la mayor parte del trabajo, cómo diseñar límites entre capas que permitan un refactorizado seguro y, lo más importante, cómo decidir si el FSD completo vale la pena para su aplicación o si un subconjunto más ligero es la mejor opción.
Por qué las carpetas basadas en tipos impiden la escalabilidad
El diseño clásico agrupa los archivos según su tipo (componentes, ganchos, utilidades) en lugar de según su función. Eso está bien para una aplicación pequeña, pero una vez que aparecen dominios empresariales reales, comienza a verse así:
src/
components/ # 200 files, no domain grouping
hooks/ # useAuth next to useCartTotals
utils/ # the grab-bag
services/ # api calls that import from utils, which imports back
Dos cosas se deterioran en una estructura como esta. Primero, la pregunta “¿A dónde pertenece este archivo?” no tiene una respuesta clara, por lo que todo lo ambiguo termina en utils/. Segundo, nada restringe la dirección de las dependencias, lo que permite que se formen ciclos. Imagine que el servicio de pedidos hace referencia a utils/user.ts, que a su vez depende de services/auth.ts, y que el servicio de autenticación vuelve a importar el servicio de pedidos. Estos bucles son silenciosos; por lo general, se manifiestan mucho más tarde como un valor inesperadamente undefined cuando se carga por primera vez un módulo, ya que uno de los archivos del ciclo fue evaluado antes de que terminara de procesarse el archivo del cual depende, y para entonces el grafo es demasiado complejo como para entenderlo.
FSD aborda ambos problemas al darle a todo el proyecto una estructura predefinida en lugar de depender de los hábitos de cada desarrollador. En el momento de escribir esto, la versión 2.1 es la actual de las especificaciones; consulte la documentación oficial para ver cambios posteriores.
Las seis capas y su contenido
FSD organiza el código en capas con un orden fijo, de arriba hacia abajo: App, Pages, Widgets, Features, Entities, Shared. También existe una capa antigua llamada Processes, pero está obsoleta y puede ignorarse. Este orden no es meramente estético; define quién puede depender de quién.
- App: enrutamiento, puntos de entrada, estilos globales y proveedores.
- Pages: páginas completas o secciones grandes de la interfaz de usuario vinculadas a una ruta.
- Widgets: bloques grandes e independientes de la interfaz de usuario que por sí solos cumplen un caso de uso completo.
Las cuatro capas intermedias (Páginas hasta Entidades) se dividen en rebanadas, una por cada dominio empresarial, por ejemplo entities/user o features/auth. Las capas Aplicación y Compartido no tienen rebanadas. Cada rebanada se divide luego en segmentos según su función técnica: ui para componentes, api para solicitudes y tipos de respuesta del backend, model para esquemas y almacenes, lib para ayudantes utilizados únicamente dentro de esa rebanada, y config.
La regla de importación que impide los ciclos
La regla que sustenta todo el sistema es sencilla: un módulo solo puede importar desde capas estrictamente inferiores a la suya. Una página puede utilizar una función, pero una función nunca puede utilizar una página. Además, las secciones de la misma capa no pueden importarse entre sí en absoluto. Dentro de una sola sección, los archivos pueden hacer referencia mutua libremente; la restricción aplica más allá de los límites entre secciones y capas.
Aplicando esa regla al ciclo mencionado anteriormente, tras volver a dividir la estructura, la lógica de ordenamiento se encuentra en entities/order, la lógica de usuario en entities/user y el flujo de autenticación en features/auth. Las dependencias permitidas pasan a ser:
features/auth → can import entities, shared
entities/order → can import shared only
entities/user → can import shared only
Si ahora entities/order intenta importar desde features/auth, eso constituye una violación estructural de la arquitectura y no solo un problema de estilo. No puede formarse un ciclo, ya que cada enlace en el grafo de dependencias apunta hacia abajo. Esto también modifica la forma en que se analiza el código: en lugar de rastrear las importaciones archivo por archivo, se piensa en términos de seis capas, lo cual es un modelo que una persona puede realmente recordar.
Las APIs públicas mantienen privados los detalles internos de las capas
Cada capa expone una API pública, generalmente un index.ts que vuelve a exportar lo que pueden utilizar los usuarios externos. La documentación de FSD es clara al respecto: solo se debe exponer lo necesario y mantener ocultos los detalles de implementación.
Una versión muy común de este archivo anula el propósito original:
// features/auth/index.ts (the anti-pattern)
export * from "./ui";
export * from "./model";
Los comodines reexportan todo, incluyendo model/tokenStore.ts, que se suponía que debía permanecer privado. Tarde o temprano alguna página importará directamente el almacén de tokens, y a partir de ese momento no se podrá reestructurar sin afectar a algún consumidor del que no se tenía conocimiento. Los archivos protegidos por comodines también pueden dificultar que los herramientas de empaquetado realicen el proceso de eliminación de código no utilizado, por lo que este podría terminar enviándose a los usuarios.
La alternativa enumera cada exportación de forma explícita:
// features/auth/index.ts (the boundary)
export { LoginForm } from "./ui/LoginForm";
export { useSession } from "./model/useSession";
// tokenStore stays internal. Nobody outside auth can touch it.
La estructura actual cuenta con exactamente dos nombres públicos. Todo lo demás dentro de ella puede ser renombrado, dividido o reescrito sin afectar a ningún llamante. Esa combinación, un gráfico de dependencias legible más la libertad para modificar los componentes internos, constituye la principal ventaja que promete FSD.
Existe una advertencia que la documentación de FSD plantea: los archivos index de tipo barrel pueden generar importaciones circulares, causar problemas de optimización en shared/ui y shared/lib, y ralentizar los herramientas de empaquetado en proyectos grandes. La solución práctica es mantener las APIs públicas explícitas y mínimas, y ser especialmente cuidadosos en la sección Shared, donde es mejor importar directamente muchos módulos pequeños e independientes en lugar de hacerlo a través de un único archivo barrel grande.
Importaciones cruzadas, segmentación excesiva y permitir que el linter proponga cambios
La notación @x para entidades que se refieren entre sí
Algunas entidades dependen legítimamente unas de otras; por ejemplo, un pedido necesita el tipo User. Dado que están prohibidas las importaciones dentro de la misma capa, la versión 2.1 estandarizó la notación @x precisamente para esta situación. El segmento user publica un punto de entrada dedicado destinado al segmento order, y este último importa de él:
// entities/user/@x/order.ts
export type { User } from "../model/types";
// in entities/order/model/types.ts
import type { User } from "entities/user/@x/order";
El nombre del archivo deja explícita y facilita la búsqueda de esta relación: entities/user/@x/order.ts es la parte de user que order puede ver. La documentación considera esto una excepción limitada. Mantenga las importaciones cruzadas al mínimo y utilice @x únicamente en la capa de Entidades. Si se da cuenta de que lo necesita en todo el código, eso indica que sus segmentos están definidos en los lugares incorrectos.
Steiger marca tanto las violaciones como el sobrediseño
Las reglas solo son útiles si se aplican. Steiger, el verificador de arquitectura para FSD (sus reglas se entregan como @feature-sliced/steiger-plugin), funciona sin configuración y aún estaba en versión beta en el momento de redactar este texto. Además de verificar las reglas de importación, evita el sobrediseño: la regla insignificant-slice se activa cuando una función o entidad solo tiene una página que la utiliza, mientras que excessive-slicing indica cuando el proyecto se ha dividido en demasiadas partes pequeñas. Cuando Steiger informe sobre un fragmento insignificante, tome la señal como indicación de que creó una carpeta cuando habría sido suficiente con una función local.
Cuando FSD completo es un exceso
Los nombres de las carpetas son la parte menos importante de FSD. Copiar las seis capas en una aplicación pequeña genera su propio tipo de desorden: más archivos que lógica, y cada función dispersa en cuatro directorios, de modo que un cambio sencillo se convierte en una tarea de búsqueda.
La especificación reconoce esto, y por eso la versión 2.1 recomienda un enfoque de “páginas primero” para su adopción. Comience únicamente con App, Pages, Widgets y Shared, y considere detenerse allí. Para muchas aplicaciones, eso por sí solo ya constituye una estructura completa y legible.
Mueva el código a Features y Entities solo cuando haya una razón concreta. Una heurística útil es esperar hasta que la misma lógica sea necesaria en tres o más páginas o widgets antes de asignarle su propio apartado. Hasta entonces, un flujo de pago que se encuentre completamente dentro de pages/checkout es la opción correcta, no un atajo. Una sola reutilización puede ser casualidad; tres indican un patrón real, y representar esos patrones reutilizables es precisamente el propósito de la capa Features.
Si también está considerando FSD frente a estructuras más simples basadas en carpetas de características o en dominios, nuestra comparación de patrones de estructura de carpetas en React muestra las alternativas una al lado de la otra.
Una breve lista de verificación para adoptarlo
- Adopte FSD por completo cuando el equipo tenga más de unos pocos ingenieros, el número de dominios empresariales siga aumentando y
utils/ya se haya convertido en un lugar de acumulación de código. - En el caso de un producto pequeño con un único responsable claro, mantenga la regla de importación descendente y APIs públicas simplificadas, evitando capas adicionales.
- Escriba cada
index.tscomo una lista explícita de exportaciones; evite usarexport *. - Reserve
@xpara referencias de entidad a entidad y considere su uso frecuente como un problema relacionado con los límites de diseño. - Ejecute un herramienta de análisis como Steiger para que las reglas y las advertencias sobre slices innecesarios se apliquen automáticamente.
Conclusión
Los seis carpetas nunca fueron el objetivo. Lo que realmente ofrece FSD es un grafo de dependencias que fluye solo en una dirección y capas cuyos detalles internos están ocultos detrás de pequeños contratos, los cuales juntos mantienen un código comprensible incluso dentro de un año. Puedes obtener la mayor parte de ese valor únicamente con las reglas de importación y las APIs públicas, y luego agregar capas solo cuando sea realmente necesario por motivos de reutilización.