Qué significa “calidad de código” en una app React Native
En un proyecto profesional, la calidad de código se refleja en tres resultados: (1) cambios seguros (puedes refactorizar sin miedo), (2) comportamiento predecible (errores controlados y consistentes), y (3) escalabilidad del equipo (convenciones claras y estructura fácil de navegar). Para lograrlo, combinaremos estándares de arquitectura (estructura por features, separación de responsabilidades, tipado y convenciones) con una estrategia de testing (unitario, componentes y flujos críticos) que actúe como red de seguridad.
Estándares del proyecto: estructura por features
Objetivo
Evitar carpetas genéricas gigantes (components, screens, utils) que crecen sin control. En su lugar, agrupar por “feature” (dominio funcional) para que cada parte sea localizable, testeable y mantenible.
Estructura recomendada
src/ app/ features/ auth/ api/ components/ hooks/ screens/ state/ types.ts __tests__/ profile/ ... shared/ components/ hooks/ lib/ types/ services/ http/ storage/ config/ features/<feature>: todo lo específico del dominio (UI, lógica, tipos, tests).shared/: piezas reutilizables y agnósticas del dominio (botones, inputs, helpers puros).services/: infraestructura transversal (cliente HTTP, logging, almacenamiento, analytics), sin depender de UI.config/: configuración (endpoints, flags, entornos).
Reglas de dependencia (para evitar acoplamiento)
sharedno importa desdefeatures.- Una feature no debería importar directamente otra feature; si es necesario, extrae a
sharedo crea una capa de “orquestación” enapp/. - La UI no debería conocer detalles de HTTP; usa servicios o repositorios.
Separación de responsabilidades: UI, lógica y datos
Patrón práctico: “Container + Presentational” (sin dogmas)
Separa componentes que obtienen datos/ejecutan acciones (container) de componentes que solo renderizan (presentational). Esto reduce complejidad y facilita pruebas.
// features/profile/components/ProfileView.tsx (presentational) import React from 'react'; import { View, Text, Button } from 'react-native'; type Props = { name: string; email: string; onRefresh: () => void; loading?: boolean; error?: string | null; }; export function ProfileView({ name, email, onRefresh, loading, error }: Props) { return ( <View> <Text testID="name">{name}</Text> <Text testID="email">{email}</Text> {error ? <Text testID="error">{error}</Text> : null} <Button title={loading ? 'Cargando...' : 'Actualizar'} onPress={onRefresh} disabled={loading} /> </View> ); }// features/profile/screens/ProfileScreen.tsx (container) import React from 'react'; import { ProfileView } from '../components/ProfileView'; import { useProfile } from '../hooks/useProfile'; export function ProfileScreen() { const { profile, refresh, loading, error } = useProfile(); return ( <ProfileView name={profile?.name ?? ''} email={profile?.email ?? ''} onRefresh={refresh} loading={loading} error={error} /> ); }Servicios y “repositorios” para datos
Centraliza el acceso a datos en módulos que puedan mockearse fácilmente en tests. Evita llamar fetch/axios directamente desde componentes.
// features/profile/api/profileRepository.ts export type Profile = { id: string; name: string; email: string; }; export interface ProfileRepository { getProfile(): Promise<Profile>; } // features/profile/api/profileRepository.http.ts import { httpClient } from '../../../services/http/httpClient'; import type { Profile, ProfileRepository } from './profileRepository'; export const profileRepositoryHttp: ProfileRepository = { async getProfile() { return httpClient.get<Profile>('/me'); }, };Tipado (TypeScript) para reducir bugs y mejorar refactors
Qué tipar primero (impacto alto)
- Modelos de dominio:
Profile,Order, etc. - Contratos de servicios/repositorios: interfaces con métodos.
- Props de componentes presentacionales.
- Respuestas de API (idealmente mapeadas a dominio).
Evita “any” con estrategias simples
- Usa genéricos en el cliente HTTP:
httpClient.get<T>(). - Define tipos para estados de carga y error.
- Si un dato viene “sucio” de API, crea un mapper que valide/normalice.
// shared/types/result.ts export type AppError = { code: string; message: string; cause?: unknown; }; export type Result<T> = { ok: true; data: T } | { ok: false; error: AppError };Convenciones de nombres y organización de archivos
| Elemento | Convención | Ejemplo |
|---|---|---|
| Componentes | PascalCase | ProfileView.tsx |
| Hooks | camelCase con prefijo use | useProfile.ts |
| Servicios/Repos | camelCase + sufijo | profileRepositoryHttp.ts |
| Tests | .test.ts/.test.tsx | ProfileView.test.tsx |
| Mocks | prefijo mock o carpeta __mocks__ | mockProfileRepository.ts |
Regla práctica: el nombre del archivo debe anticipar su responsabilidad. Si un archivo empieza a hacer “de todo”, es señal de refactor.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Manejo consistente de errores
Problema típico
Errores tratados de forma distinta en cada pantalla: a veces alert, a veces texto, a veces se ignoran. Esto genera UX inconsistente y tests frágiles.
Estándar: un tipo de error de app + normalización
Define un formato único (AppError) y una función que convierta errores desconocidos (HTTP, parsing, timeouts) a ese formato.
// shared/lib/errors/normalizeError.ts import type { AppError } from '../../types/result'; export function normalizeError(e: unknown): AppError { if (e && typeof e === 'object' && 'message' in e) { return { code: 'UNKNOWN', message: String((e as any).message), cause: e }; } return { code: 'UNKNOWN', message: 'Ocurrió un error inesperado', cause: e }; }Patrón para hooks: estado de carga + error tipado
// features/profile/hooks/useProfile.ts import { useCallback, useEffect, useState } from 'react'; import type { ProfileRepository, Profile } from '../api/profileRepository'; import { profileRepositoryHttp } from '../api/profileRepository.http'; import { normalizeError } from '../../../shared/lib/errors/normalizeError'; export function useProfile(repo: ProfileRepository = profileRepositoryHttp) { const [profile, setProfile] = useState<Profile | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const refresh = useCallback(async () => { setLoading(true); setError(null); try { const data = await repo.getProfile(); setProfile(data); } catch (e) { const appError = normalizeError(e); setError(appError.message); } finally { setLoading(false); } }, [repo]); useEffect(() => { refresh(); }, [refresh]); return { profile, refresh, loading, error }; }Ventaja: la UI recibe siempre loading y error con el mismo significado, lo que simplifica componentes y pruebas.
Estrategia de testing: pirámide práctica
Qué probar y por qué
- Unitarias (muchas): funciones puras, normalización de errores, mappers, reglas de negocio.
- Componentes (algunas): render, estados (loading/error), callbacks, accesibilidad básica.
- Flujos críticos (pocas, pero clave): login/checkout/guardado, navegación esencial, escenarios de error.
Herramientas habituales
jestpara unit tests y mocks.@testing-library/react-nativepara pruebas de componentes orientadas a comportamiento.- Para flujos end-to-end, una opción común es Detox (si tu proyecto lo adopta).
Configuración mínima de Jest (referencia)
// jest.config.js module.exports = { preset: 'react-native', testMatch: ['**/?(*.)+(test).[tj]s?(x)'], setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], clearMocks: true, };Recomendación: mantén la configuración simple y agrega ajustes solo cuando un caso real lo requiera.
Pruebas unitarias: ejemplo con normalización de errores
// shared/lib/errors/normalizeError.test.ts import { normalizeError } from './normalizeError'; test('devuelve mensaje genérico si no hay message', () => { const err = normalizeError(123); expect(err.code).toBe('UNKNOWN'); expect(err.message).toBe('Ocurrió un error inesperado'); }); test('usa message si existe', () => { const err = normalizeError(new Error('Fallo de red')); expect(err.message).toBe('Fallo de red'); });Pruebas de componentes: estados y callbacks
Prueba el comportamiento observable: qué se muestra y qué pasa cuando el usuario interactúa. Evita testear implementación interna.
// features/profile/components/ProfileView.test.tsx import React from 'react'; import { render, fireEvent } from '@testing-library/react-native'; import { ProfileView } from './ProfileView'; test('muestra nombre y email', () => { const { getByTestId } = render( <ProfileView name="Ada" email="ada@mail.com" onRefresh={() => {}} /> ); expect(getByTestId('name').props.children).toBe('Ada'); expect(getByTestId('email').props.children).toBe('ada@mail.com'); }); test('dispara onRefresh al presionar', () => { const onRefresh = jest.fn(); const { getByText } = render( <ProfileView name="Ada" email="ada@mail.com" onRefresh={onRefresh} /> ); fireEvent.press(getByText('Actualizar')); expect(onRefresh).toHaveBeenCalledTimes(1); }); test('muestra error si existe', () => { const { getByTestId } = render( <ProfileView name="Ada" email="ada@mail.com" error="No se pudo cargar" onRefresh={() => {}} /> ); expect(getByTestId('error').props.children).toBe('No se pudo cargar'); });Mocks de servicios: repositorios falsos para tests deterministas
Mock manual (recomendado para claridad)
// features/profile/__tests__/mockProfileRepository.ts import type { ProfileRepository } from '../api/profileRepository'; export function mockProfileRepository(overrides?: Partial<ProfileRepository>): ProfileRepository { return { getProfile: async () => ({ id: '1', name: 'Ada', email: 'ada@mail.com' }), ...overrides, }; }Test del hook con repositorio mockeado
En lugar de depender de red, inyecta el repositorio. Esto hace el test rápido y confiable.
// features/profile/hooks/useProfile.test.ts import { renderHook, act } from '@testing-library/react-native'; import { useProfile } from './useProfile'; import { mockProfileRepository } from '../__tests__/mockProfileRepository'; test('carga perfil al iniciar', async () => { const repo = mockProfileRepository(); const { result } = renderHook(() => useProfile(repo)); // espera a que el efecto termine await act(async () => {}); expect(result.current.profile?.name).toBe('Ada'); expect(result.current.error).toBe(null); }); test('maneja error del repositorio', async () => { const repo = mockProfileRepository({ getProfile: async () => { throw new Error('Boom'); } }); const { result } = renderHook(() => useProfile(repo)); await act(async () => {}); expect(result.current.profile).toBe(null); expect(result.current.error).toBe('Boom'); });Pruebas de flujos críticos: enfoque por “casos de negocio”
Selecciona 3–5 flujos que no pueden fallar
- Inicio de sesión (éxito y credenciales inválidas).
- Compra/checkout (éxito, pago rechazado, reintento).
- Guardar cambios de perfil (validación, error de red, reintento).
- Recuperación ante error (pantalla muestra estado y permite reintentar).
Ejemplo: flujo crítico a nivel de integración (sin E2E completo)
Idea: renderizar una pantalla contenedora con dependencias mockeadas y simular interacción. Esto cubre wiring entre UI + hook + repositorio sin depender de backend real.
// features/profile/screens/ProfileScreen.integration.test.tsx import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react-native'; import { ProfileView } from '../components/ProfileView'; import { useProfile } from '../hooks/useProfile'; jest.mock('../hooks/useProfile'); const useProfileMock = useProfile as jest.Mock; test('al presionar Actualizar llama refresh', async () => { const refresh = jest.fn(); useProfileMock.mockReturnValue({ profile: { id: '1', name: 'Ada', email: 'ada@mail.com' }, refresh, loading: false, error: null, }); const { getByText } = render( <ProfileView name="Ada" email="ada@mail.com" onRefresh={refresh} /> ); fireEvent.press(getByText('Actualizar')); await waitFor(() => expect(refresh).toHaveBeenCalled()); });Nota: aquí se muestra el patrón de integración con mocks. En tu proyecto real, lo ideal es renderizar ProfileScreen y mockear el repositorio inyectado (o el módulo del repositorio) para cubrir el flujo completo sin red.
Refactor guiado: mejorar mantenibilidad sin cambiar comportamiento
Escenario inicial (código difícil de testear)
Un componente que hace todo: llama API, maneja loading/error, transforma datos y renderiza. Esto complica el testing porque necesitas montar UI para validar lógica y además mockear red dentro del componente.
// ANTES: ProfileScreen.tsx (anti-patrón) import React, { useEffect, useState } from 'react'; import { View, Text, Button } from 'react-native'; import { httpClient } from '../../services/http/httpClient'; export function ProfileScreen() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); async function load() { setLoading(true); setError(null); try { const data = await httpClient.get<any>('/me'); setName(data.name); setEmail(data.email); } catch (e: any) { setError(e?.message ?? 'Error'); } finally { setLoading(false); } } useEffect(() => { load(); }, []); return ( <View> <Text>{name}</Text> <Text>{email}</Text> {error ? <Text>{error}</Text> : null} <Button title={loading ? 'Cargando...' : 'Actualizar'} onPress={load} /> </View> ); }Paso 1: extraer contrato de datos (repositorio)
Sin cambiar UI, crea ProfileRepository y una implementación HTTP. Beneficio: puedes mockearlo en tests.
// DESPUÉS (paso 1): features/profile/api/profileRepository.ts export type Profile = { id: string; name: string; email: string; }; export interface ProfileRepository { getProfile(): Promise<Profile>; }Paso 2: mover lógica a un hook
El componente deja de saber de HTTP y se vuelve un “orquestador” simple. Beneficio: testear lógica sin renderizar UI compleja.
// DESPUÉS (paso 2): features/profile/hooks/useProfile.ts // (igual al mostrado antes, con inyección de repo)Paso 3: separar UI en componente presentacional
Beneficio: pruebas de UI rápidas y enfocadas (render/estados/callbacks) sin dependencias externas.
// DESPUÉS (paso 3): ProfileView + ProfileScreen (igual al mostrado antes)Paso 4: mejorar tipado y eliminar “any”
- Reemplaza
anyporProfile. - Normaliza errores con
normalizeError. - Agrega
testIDen elementos clave para tests estables.
Paso 5: añadir tests como red de seguridad
- Unit test para
normalizeError. - Component test para
ProfileView(render, error, botón). - Hook test para
useProfilecon repositorio mock.
Checklist de calidad para PRs (pull requests)
- ¿La feature está encapsulada en
features/<feature>y no dispersa en carpetas genéricas? - ¿La UI no conoce detalles de infraestructura (HTTP/storage)?
- ¿Los errores se normalizan y se muestran de forma consistente?
- ¿No hay
anyinnecesarios? ¿Los modelos y contratos están tipados? - ¿Hay tests para lógica crítica y estados (éxito/error/loading)?
- ¿Los nombres de archivos y funciones reflejan responsabilidad única?
- ¿El refactor mantiene comportamiento (tests pasan) y reduce complejidad?