secture & code

Arquitectura limpia en React Native: Cómo crear apps mantenibles

Cuando desarrollas una aplicación móvil con React Native, el primer reto con el que te topas no es técnico en el sentido de «qué librería uso». Es estructural. El framework te empuja constantemente hacia el acoplamiento. Ves un formulario, lo bindeas directamente a una llamada a la API, y listo, funciona. Es cómodo, es rápido, y es veneno a largo plazo.

¿Por qué veneno? Porque el día que los requisitos cambian (y siempre cambian) te encuentras con que modificar el comportamiento de la app te obliga a tocar la presentación, y cambiar cómo se ve una pantalla te obliga a arriesgar la lógica de negocio. Todo está pegado con todo. Cada cambio es una operación a corazón abierto.

Aquí está el típico componente que todos hemos escrito alguna vez y que resume el problema:

✗ El antipatrón: todo pegado con todo
function CreateUserScreen() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = async () => {
    // Validación de negocio mezclada con la UI
    if (!email.includes('@')) {
      alert('Email inválido');
      return;
    }
    // La pantalla conoce Firebase directamente
    await firebase.firestore().collection('users').add({ name, email });
  };

  return (/* ... campos y botón ... */);
}

Este componente sabe demasiado: sabe qué es un email válido, sabe que existe Firebase, sabe cómo se estructura la colección. El día que cambies de proveedor o que la regla del email evolucione, vuelves aquí, a un fichero visual, a tocar lógica. Eso no escala.

La buena noticia es que esto está resuelto desde hace años. Los patrones de arquitectura que llevamos usando en backend, como la arquitectura hexagonal (Ports & Adapters) o la arquitectura limpia, aplican igual de bien en una app móvil. La idea de fondo es simple: separar el código en capas, donde cada una tiene una responsabilidad clara y un cambio en una capa no se propaga en cascada hacia las demás.


Las cuatro capas de una arquitectura limpia en React Native

En un escenario como este trabajamos con cuatro capas.

La capa de dominio es el corazón. Aquí vive la lógica de negocio pura: entidades, casos de uso, reglas. No hay frameworks, no hay librerías externas, no hay nada que no sea lógica. Un usuario no puede tener un email inválido, un perfil tiene un nombre y una foto. Estas entidades no saben que existe una base de datos ni que existe React Native.

domain/user/User.ts
export class User {
  private constructor(
    public readonly name: string,
    public readonly email: string,
  ) {}

  // La regla de negocio vive aquí, no en la pantalla
  static create(name: string, email: string): User {
    if (!email.includes('@')) {
      throw new InvalidEmailError(email);
    }
    return new User(name, email);
  }
}

La capa de aplicación es el director de orquesta. Coordina el dominio: toma una petición, valida que lleguen los datos correctos, llama a los casos de uso que hagan falta y decide qué guardar y cuándo. Sigue siendo agnóstica del framework. Y, fundamental, depende de una interfaz, no de una implementación concreta:

domain/user/UserRepository.ts (el «puerto», en Dominio) + application/CreateUser.ts (el caso de uso)
// El "puerto": un contrato, no una implementación
export interface UserRepository {
  save(user: User): Promise<void>;
}

// El caso de uso: orquesta, sin saber quién implementa el puerto
export class CreateUser {
  constructor(private readonly repository: UserRepository) {}

  async execute(name: string, email: string): Promise<void> {
    const user = User.create(name, email); // valida en dominio
    await this.repository.save(user); // persiste vía interfaz
  }
}

La capa de infraestructura es donde viven las dependencias externas, todo lo que no controlas. La base de datos, las APIs, los sensores del teléfono, el sistema de archivos. Si necesitas hablar con Firebase o leer la cámara, la implementación va aquí. Es el «adaptador» que cumple el contrato que definió el dominio:

infrastructure/FirebaseUserRepository.ts (el «adaptador»)
export class FirebaseUserRepository implements UserRepository {
  async save(user: User): Promise<void> {
    await firebase.firestore().collection('users').add({
      name: user.name,
      email: user.email,
    });
  }
}

Fíjate en el detalle clave: si mañana migras a SQLite, escribes un SQLiteUserRepository que implemente la misma interfaz y ya está. Ni el caso de uso ni la pantalla se enteran de nada.

Y la capa de presentación son tus componentes React Native: las pantallas, los botones, los hooks, todo lo visual. Esta capa consume la de aplicación y nada más. No accede directamente a dominio ni a infraestructura. Compara este componente con el del principio:

✓ presentation/CreateUserScreen.tsx
function CreateUserScreen({ createUser }) { // recibe el caso de uso inyectado
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = async () => {
    try {
      await createUser.execute(name, email);
    } catch (e) {
      alert(e.message); // la pantalla solo presenta, no decide reglas
    }
  };

  return (/* ... campos y botón ... */);
}

La pantalla ya no sabe qué es un email válido ni sabe que existe Firebase. Solo recoge datos, llama al caso de uso y presenta el resultado. Eso es todo lo que debería hacer.


Cómo se traduce esto en carpetas

La estructura es un reflejo directo de las capas. Tienes una carpeta src y dentro la divides en domain, application, infrastructure y presentation. Dentro de cada una puedes subdividir por feature.

src/
├── domain/
│   ├── user/
│   │   ├── User.ts
│   │   └── UserRepository.ts
│   └── profile/
├── application/
│   └── CreateUser.ts
├── infrastructure/
│   ├── FirebaseUserRepository.ts
│   └── http/
└── presentation/
    ├── screens/
    ├── components/
    └── hooks/

La ventaja es de orientación pura. Cuando alguien nuevo abre el proyecto entiende en segundos dónde buscar. ¿Necesito cambiar cómo se guarda un usuario? Voy a infrastructure. ¿Necesito cambiar el flujo de creación? Voy a application. ¿Necesito retocar la pantalla? Voy a presentation. No hay que ir adivinando.


La regla de oro: las dependencias apuntan hacia adentro

Aquí está el principio que sostiene todo lo demás, y no es un invento reciente: es la Regla de Dependencia que formuló Robert C. Martin, construida a su vez sobre la Arquitectura Hexagonal (Ports & Adapters) que Alistair Cockburn propuso en 2005. La formulación es tajante: las dependencias del código fuente solo pueden apuntar hacia adentro, y nada en un círculo interior puede conocer nada de un círculo exterior.

En la práctica: presentación depende de aplicación. Aplicación depende de dominio. Y dominio no depende de nada. Nunca.

El antipatrón que se ve una y otra vez en React Native es justo el componente del principio: importa directamente un repositorio o un cliente HTTP y dispara una petición desde el propio botón. Eso es acoplamiento brutal, porque el día que cambias de proveedor de API te toca ir a tocar componentes visuales. No tiene ningún sentido.

La solución son las abstracciones. En lugar de que la app conozca implementaciones concretas, defines interfaces: contratos que dicen qué métodos debe tener un repositorio o un cliente, pero no cómo se implementan. La aplicación trabaja contra esas interfaces, y la implementación real vive escondida en infraestructura. El mecanismo que lo hace posible es la inversión de dependencias: los casos de uso programan contra una interfaz (el puerto) y es el adaptador de la capa externa quien aporta la clase concreta. Justo lo que has visto arriba con CreateUser y UserRepository.

La pieza que cierra el círculo es la inyección de dependencias: los casos de uso reciben sus dependencias desde afuera en lugar de crearlas ellos mismos. Por eso en el componente le pasábamos createUser ya construido en lugar de instanciarlo dentro. Así el núcleo de tu app nunca queda atado a una tecnología concreta.


En resumen

Arquitectura limpia en móvil no es sobre-ingeniería ni ceremonia por la ceremonia. Es reconocer que la comodidad de acoplarlo todo se paga carísima con el tiempo. Los objetivos que persigue son muy concretos: independencia del framework, independencia de la UI, independencia de la base de datos, y una app testeable sin necesidad de levantar la interfaz ni la persistencia. Separas en capas, haces que las dependencias apunten siempre hacia adentro, trabajas contra abstracciones en lugar de implementaciones, y de repente tu app puede evolucionar sin que cada cambio sea un campo de minas.


Bibliografía y referencias

  • Martin, Robert C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall / Pearson. — Fuente canónica de las cuatro capas concéntricas y de la Regla de Dependencia.
  • Cockburn, Alistair (2005). «Hexagonal Architecture (Ports and Adapters)». — Raíz conceptual de la que bebe la arquitectura limpia; el modelo de puertos y adaptadores sobre el que Martin construyó su propuesta.
Arquitectura limpia en React Native

Si te interesan más artículos como este, encuéntralos en nuestro blog

Full-Stack Developer

Imagen de Vicent Gisbert Soto

Vicent Gisbert Soto

En esta vida todo se entrena
Imagen de Vicent Gisbert Soto

Vicent Gisbert Soto

En esta vida todo se entrena

We are HIRING!

What Can We Do