Cuando empiezas un proyecto React Native desde cero, la pregunta llega rápido: ¿cómo organizo los componentes?
No tiene una respuesta única. Dos proyectos distintos (uno una app bancaria de gran escala, otro una app de métricas de vehículo construida desde cero), me llevaron a respuestas completamente diferentes, y ambas eran correctas en su contexto. Este artículo explora esas dos filosofías: Atomic Design y la organización feature-based.
1. Qué es Atomic Design
Atomic Design es una metodología propuesta por Brad Frost para construir sistemas de diseño modulares. La idea central es que las interfaces no son páginas monolíticas, sino conjuntos de piezas reutilizables organizadas en niveles de complejidad creciente.
La analogía es química: igual que la materia se compone de átomos que forman moléculas, que a su vez forman estructuras más complejas, los componentes de UI se construyen combinando piezas más pequeñas y simples.
Los cinco niveles originales son:
- Atoms: el nivel más pequeño. Un botón, un campo de texto, un icono, un badge. No dependen de ningún otro componente.
- Molecules: combinaciones simples de átomos con un propósito concreto. Un campo de formulario (label + input + mensaje de error), un ítem de lista con icono y texto.
- Organisms: Secciones completas y relativamente independientes. Una cabecera de pantalla, un listado de transacciones, un formulario de login.
- Templates: La estructura de pantalla sin datos reales. Define el layout de los organismos.
- Pages: Los templates con datos reales. En React Native, esto suele mapear directamente con las screens del navegador.
Atomic Design fue pensado originalmente para web. En proyectos móviles con React Native aparecen variantes: algunos equipos separan primitives (wrappers del sistema de diseño) y objects (componentes con lógica de negocio integrada) como capas adicionales.
2. Atomic Design en la práctica: la variante con primitives y objects
En un proyecto bancario de gran escala en el que trabajé (una app con más de una docena de dominios, múltiples entornos de build y soporte bilingüe) el equipo adoptó una variante del modelo de Frost que añade dos capas: primitives y objects.
La estructura de carpetas de componentes UI quedaba así:
1src/
2└── components/
3 ├── primitives/ # Wrappers del design system
4 ├── atoms/ # Componentes elementales
5 ├── molecules/ # Combinaciones simples
6 ├── organisms/ # Secciones complejas
7 └── objects/ # Componentes con lógica de negocioCada capa tiene una responsabilidad clara.
Primitives: el design system encapsulado
Los primitives son wrappers sobre los componentes de React Native que encapsulan el sistema de diseño: tipografía, espaciado, colores, sombras. No son componentes de UI per se, sino que son las piezas de construcción que garantizan consistencia visual.
1// primitives/Typography.tsx
2type TypographyVariant = 'heading1' | 'heading2' | 'body' | 'caption' | 'label'
3
4interface TypographyProps {
5 variant: TypographyVariant
6 children: React.ReactNode
7 color?: string
8}
9
10const styles: Record<TypographyVariant, TextStyle> = {
11 heading1: { fontSize: 24, fontWeight: '700', lineHeight: 32 },
12 heading2: { fontSize: 20, fontWeight: '600', lineHeight: 28 },
13 body: { fontSize: 16, fontWeight: '400', lineHeight: 24 },
14 caption: { fontSize: 12, fontWeight: '400', lineHeight: 18 },
15 label: { fontSize: 14, fontWeight: '500', lineHeight: 20 },
16}
17
18export function Typography({ variant, children, color }: TypographyProps) {
19 return (
20 <Text style={[styles[variant], color ? { color } : undefined]}>
21 {children}
22 </Text>
23 )
24}La ventaja de tener primitives es que cambiar el design system no implica tocar cientos de componentes. Cambias Typography en un sitio y la actualización se propaga sola.
Atoms: sin dependencias, máxima reutilización
Los atoms son los componentes más simples: no componen otros componentes de la app, tienen una única responsabilidad y son completamente independientes del dominio.
1// atoms/Badge.tsx
2type BadgeVariant = 'success' | 'warning' | 'error' | 'info' | 'neutral'
3
4interface BadgeProps {
5 variant: BadgeVariant
6 label: string
7 size?: 'sm' | 'md'
8}
9
10const variantColors: Record<BadgeVariant, { bg: string; text: string }> = {
11 success: { bg: '#E8F5E9', text: '#2E7D32' },
12 warning: { bg: '#FFF8E1', text: '#F57F17' },
13 error: { bg: '#FFEBEE', text: '#C62828' },
14 info: { bg: '#E3F2FD', text: '#1565C0' },
15 neutral: { bg: '#F5F5F5', text: '#616161' },
16}
17
18export function Badge({ variant, label, size = 'md' }: BadgeProps) {
19 const { bg, text } = variantColors[variant]
20 const padding = size === 'sm' ? { paddingHorizontal: 6, paddingVertical: 2 }
21 : { paddingHorizontal: 10, paddingVertical: 4 }
22 return (
23 <View style={[styles.container, { backgroundColor: bg }, padding]}>
24 <Typography variant="caption" color={text}>{label}</Typography>
25 </View>
26 )
27}Molecules: combinaciones con propósito
Una molécula combina dos o más átomos para resolver un problema concreto de UI. El criterio clave es que no tiene lógica de negocio, solo de presentación.
1// molecules/FormField.tsx
2interface FormFieldProps {
3 label: string
4 value: string
5 onChangeText: (text: string) => void
6 error?: string
7 placeholder?: string
8 secureTextEntry?: boolean
9}
10
11export function FormField({
12 label, value, onChangeText, error, placeholder, secureTextEntry,
13}: FormFieldProps) {
14 return (
15 <View style={styles.container}>
16 <Typography variant="label">{label}</Typography>
17 <Spacer size="xs" />
18 <TextInput
19 value={value}
20 onChangeText={onChangeText}
21 placeholder={placeholder}
22 secureTextEntry={secureTextEntry}
23 style={[styles.input, error ? styles.inputError : undefined]}
24 />
25 {error && (
26 <>
27 <Spacer size="xs" />
28 <Typography variant="caption" color={colors.error}>{error}</Typography>
29 </>
30 )}
31 </View>
32 )
33}Objects: componentes con lógica de negocio
Esta es la capa que Frost no incluye en su modelo original y que resulta especialmente útil en apps de dominio complejo. Los objects son componentes que conocen el dominio: saben qué es una transacción, qué formatos de fecha usa la app, qué estados puede tener una cuenta.
1// objects/TransactionItem.tsx
2interface Transaction {
3 id: string
4 concept: string
5 amount: number
6 currency: 'EUR'
7 date: Date
8 status: 'completed' | 'pending' | 'failed'
9}
10
11interface TransactionItemProps {
12 transaction: Transaction
13 onPress: (id: string) => void
14}
15
16export function TransactionItem({ transaction, onPress }: TransactionItemProps) {
17 const sign = transaction.amount >= 0 ? '+' : ''
18 const formattedAmount = `${sign}${transaction.amount.toFixed(2)} ${transaction.currency}`
19 const badgeVariant = transaction.status === 'completed' ? 'success'
20 : transaction.status === 'pending' ? 'warning'
21 : 'error'
22
23 return (
24 <Pressable onPress={() => onPress(transaction.id)} style={styles.container}>
25 <View style={styles.content}>
26 <Typography variant="body">{transaction.concept}</Typography>
27 <Typography variant="caption" color={colors.textSecondary}>
28 {formatDate(transaction.date)}
29 </Typography>
30 </View>
31 <View style={styles.right}>
32 <Typography variant="body">{formattedAmount}</Typography>
33 <Spacer size="xs" />
34 <Badge variant={badgeVariant} label={transaction.status} size="sm" />
35 </View>
36 </Pressable>
37 )
38}Separar objects de molecules evita que las moléculas conozcan el dominio. Si FormField no sabe nada de transacciones ni cuentas, puede reutilizarse en cualquier módulo sin arrastrar dependencias de negocio.
3. El coste de la abstracción
Atomic Design brilla en equipos grandes y proyectos de larga duración. Pero tiene un coste que no siempre se menciona: la complejidad de navegación.
Para añadir un botón nuevo a una pantalla, un desarrollador junior en un proyecto atómico necesita:
- Decidir si es un
atom,moleculeuobject. - Revisar si ya existe algo similar en cualquiera de esas capas.
- Crear el componente en la carpeta correcta.
- Importarlo en el nivel superior donde se usa.
Ese proceso es razonable cuando el codebase tiene cientos de componentes compartidos. Pero en un proyecto de 3-4 meses con un solo desarrollador, la misma operación puede ser puro overhead sin beneficio real.
Atomic Design no es gratis. Introduce una taxonomía que el equipo entero debe conocer y mantener. Si el proyecto no tiene escala suficiente para amortizar ese coste, puedes acabar construyendo un design system que nadie más va a usar.
4. Feature-based: la alternativa pragmática
En una app de métricas de vehículo que construí desde cero (con lectura de datos OBD-II vía Bluetooth, gráficos en tiempo real y un módulo nativo en Kotlin para la gestión BLE) elegí una organización completamente distinta: feature-based.
La idea es simple: en lugar de organizar por tipo de componente (atoms, molecules...), organizas por funcionalidad. Todo lo que pertenece a una feature vive junto: pantallas, componentes, hooks, servicios, tipos.
1src/
2├── features/
3│ ├── bluetooth/
4│ │ ├── components/ # Componentes específicos de BLE
5│ │ │ ├── DeviceList.tsx
6│ │ │ ├── ConnectionStatus.tsx
7│ │ │ └── ScanButton.tsx
8│ │ ├── hooks/
9│ │ │ ├── useBleScanner.ts
10│ │ │ └── useDeviceConnection.ts
11│ │ ├── screens/
12│ │ │ └── BluetoothSetupScreen.tsx
13│ │ └── services/
14│ │ └── bleService.ts
15│ ├── metrics/
16│ │ ├── components/
17│ │ │ ├── MetricGauge.tsx
18│ │ │ ├── SpeedChart.tsx
19│ │ │ └── MetricCard.tsx
20│ │ ├── hooks/
21│ │ │ └── useObdMetrics.ts
22│ │ └── screens/
23│ │ └── DashboardScreen.tsx
24│ └── diagnostics/
25│ ├── components/
26│ │ └── DiagnosticCode.tsx
27│ └── screens/
28│ └── DiagnosticsScreen.tsx
29└── shared/
30 ├── components/ # Componentes genuinamente compartidos
31 ├── hooks/
32 └── utils/Por qué funcionó aquí
La app de métricas tenía tres dominios bien delimitados: la conexión BLE, la visualización de datos en tiempo real, y el módulo de diagnóstico. Cada uno era suficientemente autónomo como para vivir en su propia carpeta.
El beneficio inmediato fue la localización de cambios. Si tenía que modificar cómo se procesa un comando OBD-II, sabía exactamente dónde mirar: features/metrics/hooks/useObdMetrics.ts. No necesitaba rastrear a través de capas.
1// features/bluetooth/hooks/useBleScanner.ts
2export function useBleScanner() {
3 const [devices, setDevices] = useState<BleDevice[]>([])
4 const [isScanning, setIsScanning] = useState(false)
5
6 const startScan = useCallback(async () => {
7 setIsScanning(true)
8 setDevices([])
9
10 BleManager.scan([], 10, false).then(() => {
11 BleManager.addListener('BleManagerDiscoverPeripheral', (device) => {
12 setDevices(prev => {
13 if (prev.some(d => d.id === device.id)) return prev
14 return [...prev, device]
15 })
16 })
17 })
18 }, [])
19
20 useEffect(() => {
21 return () => { BleManager.stopScan() }
22 }, [])
23
24 return { devices, isScanning, startScan }
25}La colocation (tener el componente y su hook en la misma carpeta) reduce el tiempo de orientación cuando vuelves a un módulo después de semanas. No necesitas recordar en qué capa vive cada pieza.
La mejor arquitectura es la que permite que un desarrollador nuevo (o tú mismo tres meses después) entienda qué hace el código sin necesitar un mapa.
5. La frontera entre ambos enfoques: shared/
El talón de Aquiles del enfoque feature-based es la carpeta shared. Al principio es pequeña y disciplinada: un Button, un Text customizado, algún hook de utilidad. Con el tiempo, si no hay vigilancia, shared se convierte en un cajón de sastre donde acaban los componentes que "no saben bien dónde van".
La regla práctica que funciona: un componente pasa a shared solo cuando lo usan al menos dos features distintas. Si solo lo usa una feature, vive en esa feature aunque parezca genérico.
1shared/
2├── components/
3│ ├── Button.tsx # Usado por bluetooth + metrics + diagnostics
4│ ├── EmptyState.tsx # Usado por bluetooth + diagnostics
5│ └── LoadingOverlay.tsx # Usado en todas las screens
6├── hooks/
7│ ├── useAppTheme.ts # Accede al tema global
8│ └── usePermissions.ts # Permisos del sistema, cross-feature
9└── utils/
10 ├── formatters.ts
11 └── validators.tsEn Atomic Design, este problema se gestiona de forma diferente: los átomos y moléculas son por definición reutilizables, y los objects encapsulan la lógica de dominio. El sistema ya viene con la taxonomía integrada.
6. Cuándo usar cada enfoque
No hay una respuesta correcta universal. La elección, desde mi experiencia, depende de tres variables: tamaño del equipo, duración del proyecto y complejidad del diseño visual.
| Criterio | Atomic Design | Feature-based |
|---|---|---|
| Equipo | Múltiples desarrolladores | Uno o dos |
| Duración | Largo plazo, producto vivo | Sprint corto o duración definida |
| Design system | Existe o se va a construir | No hay diseño compartido con otras apps |
| Dominios | Muchos dominios entrelazados | Dominios bien delimitados |
| Onboarding | Requiere aprender la taxonomía | Intuitivo: buscas por funcionalidad |
Atomic Design funciona especialmente bien cuando hay un design system activo, cuando los diseñadores y desarrolladores comparten el mismo lenguaje de componentes. En ese contexto, la taxonomía no es overhead: es el vocabulario común. Un diseñador que habla de "molecules" y un desarrollador que tiene una carpeta /molecules están trabajando con el mismo mapa.
Feature-based funciona mejor cuando la velocidad de entrega es la prioridad, el equipo es pequeño, o los dominios de la app son suficientemente independientes. No requiere que el equipo conozca una taxonomía , permitiendo que cualquiera que haya trabajado con features sepa dónde buscar.
No son mutuamente excluyentes. Puedes tener un enfoque feature-based con una capa shared/ que siga principios de Atomic Design internamente. Lo importante es que la convención sea explícita y el equipo la comparta.
7. Lo que aprendí trabajando en los dos
Trabajar en ambos proyectos con estas dos filosofías me dejó algunas conclusiones claras.
En el proyecto bancario, la inversión en Atomic Design se amortizó. Cuando llegaba a un dominio nuevo (autenticación, transferencias, gestión de sesión) los componentes de UI ya estaban ahí, documentados y consistentes. La pantalla de confirmación de Bizum y la de autenticación biométrica comparten la misma paleta de atoms y molecules sin duplicación. Eso no ocurre por accidente.
En la app de métricas, la organización feature-based fue la decisión correcta desde el primer sprint. El foco era la capa de comunicación Bluetooth y el procesamiento de datos OBD-II. La UI era instrumental, no el producto. Invertir en una jerarquía atómica completa habría sido construir infraestructura que nadie iba a necesitar.
La lección no es "Atomic Design es mejor" ni "feature-based es mejor". Es que la arquitectura de componentes debe acompañar la escala del proyecto, no anticiparla. Una buena arquitectura para un equipo de diez en un producto de cinco años puede ser una trampa de complejidad para un equipo de dos en un proyecto de tres meses.
Empieza simple. Introduce estructura cuando el dolor sea real, cuando se encuentren duplicaciones, cuando nadie sepa dónde añadir un componente nuevo, cuando el onboarding de un nuevo desarrollador tome más tiempo del razonable. Ese es el momento de formalizar, no antes.
