Interfaz de usuario en React Native con componentes y estilos

Capítulo 2

Tiempo estimado de lectura: 12 minutos

+ Ejercicio

Componentes base: el “alfabeto” de la UI

En React Native, la interfaz se construye combinando componentes base. Piensa en ellos como piezas de LEGO: cada una tiene una responsabilidad clara y, al componerlas, obtienes pantallas completas. En este capítulo trabajaremos con View, Text, Image, ScrollView, FlatList, Pressable y TextInput, y aprenderás a darles forma con StyleSheet y Flexbox.

View: contenedor y estructura

View es el contenedor principal para agrupar elementos y aplicar layout (Flexbox), fondos, bordes y espaciados.

import { View, Text } from 'react-native';

export function Example() {
  return (
    <View>
      <Text>Hola</Text>
    </View>
  );
}

Text: tipografía y contenido

Text renderiza texto y permite estilos como tamaño, peso, color, alineación y espaciado entre líneas.

<Text style={{ fontSize: 18, fontWeight: '600' }}>Título</Text>
<Text style={{ color: '#6B7280', lineHeight: 20 }}>Descripción breve.</Text>

Image: imágenes locales y remotas

Image requiere definir tamaño (o un contenedor que lo determine). Para imágenes remotas se usa source={{ uri }}; para locales, require().

import { Image } from 'react-native';

<Image
  source={{ uri: 'https://picsum.photos/300/200' }}
  style={{ width: 300, height: 200, borderRadius: 12 }}
  resizeMode="cover"
/>

Propiedades útiles: resizeMode (cover, contain, stretch, center) y borderRadius para esquinas redondeadas.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

ScrollView vs FlatList: desplazamiento y rendimiento

ScrollView renderiza todo su contenido de una vez; es ideal para pantallas con contenido limitado. FlatList virtualiza filas y es preferible para listas largas.

import { ScrollView, View, Text } from 'react-native';

export function Screen() {
  return (
    <ScrollView contentContainerStyle={{ padding: 16 }}>
      <Text>Sección 1</Text>
      <View style={{ height: 600 }} />
      <Text>Sección 2</Text>
    </ScrollView>
  );
}
import { FlatList, Text, View } from 'react-native';

const data = Array.from({ length: 100 }, (_, i) => ({ id: String(i), name: `Item ${i}` }));

export function ListScreen() {
  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id}
      contentContainerStyle={{ padding: 16 }}
      ItemSeparatorComponent={() => <View style={{ height: 12 }} />}
      renderItem={({ item }) => (
        <View style={{ padding: 12, backgroundColor: '#fff', borderRadius: 12 }}>
          <Text>{item.name}</Text>
        </View>
      )}
    />
  );
}

Pressable: interacción y estados

Pressable permite responder a toques y ajustar estilos según el estado (presionado, enfocado, etc.). Es una base excelente para botones personalizados.

import { Pressable, Text } from 'react-native';

export function PrimaryButton({ title, onPress }) {
  return (
    <Pressable
      onPress={onPress}
      style={({ pressed }) => [
        {
          backgroundColor: '#111827',
          paddingVertical: 12,
          paddingHorizontal: 16,
          borderRadius: 12,
          opacity: pressed ? 0.85 : 1,
        },
      ]}
    >
      <Text style={{ color: '#fff', fontWeight: '600', textAlign: 'center' }}>{title}</Text>
    </Pressable>
  );
}

TextInput: formularios y entrada de datos

TextInput se controla normalmente con estado. Ajusta teclado, autocorrección, placeholder y seguridad para contraseñas.

import { useState } from 'react';
import { TextInput, View, Text } from 'react-native';

export function LoginForm() {
  const [email, setEmail] = useState('');

  return (
    <View>
      <Text style={{ marginBottom: 8 }}>Email</Text>
      <TextInput
        value={email}
        onChangeText={setEmail}
        placeholder="tu@email.com"
        keyboardType="email-address"
        autoCapitalize="none"
        style={{
          borderWidth: 1,
          borderColor: '#E5E7EB',
          borderRadius: 12,
          paddingHorizontal: 12,
          paddingVertical: 10,
        }}
      />
    </View>
  );
}

Estilos con StyleSheet: consistencia y mantenimiento

Aunque puedes usar estilos inline, StyleSheet ayuda a centralizar estilos, mejorar legibilidad y reutilización. Un patrón común es definir estilos por “bloques” (contenedor, texto, tarjeta, etc.).

import { StyleSheet, View, Text } from 'react-native';

export function Card() {
  return (
    <View style={styles.card}>
      <Text style={styles.title}>Producto</Text>
      <Text style={styles.subtitle}>Descripción corta</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: '#FFFFFF',
    borderRadius: 16,
    padding: 16,
    borderWidth: 1,
    borderColor: '#F3F4F6',
  },
  title: {
    fontSize: 18,
    fontWeight: '700',
    color: '#111827',
  },
  subtitle: {
    marginTop: 6,
    fontSize: 14,
    color: '#6B7280',
    lineHeight: 20,
  },
});

Guía rápida de espaciados

  • padding: espacio interno del componente.
  • margin: espacio externo respecto a otros componentes.
  • Usa escalas consistentes: por ejemplo 4, 8, 12, 16, 24, 32.
  • Prefiere paddingHorizontal/paddingVertical y marginHorizontal/marginVertical para mantener simetría.

Tipografía práctica

  • Define jerarquías: título (18–24), subtítulo (14–16), cuerpo (14–16), caption (12–13).
  • Controla legibilidad con lineHeight (aprox. 1.3–1.5 del fontSize).
  • Evita usar demasiados pesos; normalmente 400/500/700 es suficiente.

Flexbox en React Native: layouts responsivos

React Native usa Flexbox por defecto con flexDirection: 'column'. Esto significa que los elementos se apilan verticalmente si no indicas lo contrario.

Conceptos clave

PropiedadQué controlaEjemplo típico
flexDirectionEje principal (fila o columna)'row' para barras horizontales
justifyContentDistribución en el eje principal'space-between' para separar extremos
alignItemsAlineación en el eje cruzado'center' para centrar verticalmente en filas
flexCómo crece/ocupa espacioflex: 1 para llenar
gapEspacio entre hijos (si está disponible)gap: 12
flexWrapPermite saltos de línea en filas'wrap' para chips/tags

Ejemplo: encabezado con avatar y acciones

import { View, Text, Image, Pressable } from 'react-native';

export function Header() {
  return (
    <View style={{ flexDirection: 'row', alignItems: 'center', padding: 16 }}>
      <Image
        source={{ uri: 'https://picsum.photos/80' }}
        style={{ width: 44, height: 44, borderRadius: 22, marginRight: 12 }}
      />

      <View style={{ flex: 1 }}>
        <Text style={{ fontSize: 16, fontWeight: '700' }}>Hola, Ana</Text>
        <Text style={{ color: '#6B7280' }}>Bienvenida de vuelta</Text>
      </View>

      <Pressable style={{ padding: 10, borderRadius: 12, backgroundColor: '#F3F4F6' }}>
        <Text>⚙️</Text>
      </Pressable>
    </View>
  );
}

Observa el uso de flex: 1 en el bloque central para que empuje el botón de la derecha hacia el extremo.

Ejemplo: grid simple con filas y wrap

import { View, Text } from 'react-native';

const tags = ['React', 'Native', 'UI', 'Flexbox', 'Listas', 'Estilos'];

export function TagCloud() {
  return (
    <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
      {tags.map((t) => (
        <View key={t} style={{
          paddingHorizontal: 12,
          paddingVertical: 8,
          borderRadius: 999,
          backgroundColor: '#EEF2FF',
          marginRight: 8,
          marginBottom: 8,
        }}>
          <Text style={{ color: '#3730A3', fontWeight: '600' }}>{t}</Text>
        </View>
      ))}
    </View>
  );
}

Manejo de imágenes: patrones comunes

Imagen “hero” responsiva con relación de aspecto

Un patrón útil es fijar la relación de aspecto y dejar que el ancho lo determine el contenedor.

import { View, Image } from 'react-native';

export function HeroImage() {
  return (
    <View style={{ width: '100%', aspectRatio: 16 / 9, borderRadius: 16, overflow: 'hidden' }}>
      <Image
        source={{ uri: 'https://picsum.photos/1200/675' }}
        style={{ width: '100%', height: '100%' }}
        resizeMode="cover"
      />
    </View>
  );
}

overflow: 'hidden' es clave para que el borderRadius recorte la imagen.

Avatar circular

<Image
  source={{ uri: 'https://picsum.photos/200' }}
  style={{ width: 64, height: 64, borderRadius: 32 }}
/>

Patrones de composición: de piezas a pantallas

Para construir UI escalable, evita pantallas monolíticas. Divide en componentes reutilizables y organiza la pantalla por secciones. Un enfoque práctico es: (1) componentes de base (botones, inputs, separadores), (2) componentes de dominio (tarjetas de producto, item de lista), (3) secciones (header, listado, resumen), (4) pantalla.

Patrón 1: Componentes reutilizables (UI kit mínimo)

Crea componentes pequeños con API clara. Ejemplo: botón primario y campo de texto.

import { StyleSheet, Pressable, Text, TextInput, View } from 'react-native';

export function AppButton({ title, onPress, variant = 'primary' }) {
  return (
    <Pressable
      onPress={onPress}
      style={({ pressed }) => [
        styles.btn,
        variant === 'primary' ? styles.btnPrimary : styles.btnGhost,
        pressed && { opacity: 0.85 },
      ]}
    >
      <Text style={[styles.btnText, variant === 'primary' ? styles.btnTextPrimary : styles.btnTextGhost]}>
        {title}
      </Text>
    </Pressable>
  );
}

export function AppInput({ label, value, onChangeText, placeholder }) {
  return (
    <View style={{ gap: 8 }}>
      <Text style={{ fontWeight: '600', color: '#111827' }}>{label}</Text>
      <TextInput
        value={value}
        onChangeText={onChangeText}
        placeholder={placeholder}
        style={styles.input}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  btn: { paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center' },
  btnPrimary: { backgroundColor: '#111827' },
  btnGhost: { backgroundColor: '#F3F4F6' },
  btnText: { fontWeight: '700' },
  btnTextPrimary: { color: '#FFFFFF' },
  btnTextGhost: { color: '#111827' },
  input: {
    borderWidth: 1,
    borderColor: '#E5E7EB',
    borderRadius: 12,
    paddingHorizontal: 12,
    paddingVertical: 10,
    backgroundColor: '#FFFFFF',
  },
});

Patrón 2: UI por secciones (Screen Sections)

En lugar de renderizar todo en una sola función, crea secciones con responsabilidad clara: ProfileHeader, StatsRow, RecentActivityList. Esto facilita iterar sobre el diseño y testear visualmente cada bloque.

function ProfileHeader() { /* ... */ }
function StatsRow() { /* ... */ }
function RecentActivityList() { /* ... */ }

export function ProfileScreen() {
  return (
    <ScrollView contentContainerStyle={{ padding: 16, gap: 16 }}>
      <ProfileHeader />
      <StatsRow />
      <RecentActivityList />
    </ScrollView>
  );
}

Patrón 3: Item de lista como componente

Con FlatList, extrae el item a un componente para mantener la pantalla limpia y reutilizarlo en otras listas.

import { View, Text, Image, Pressable } from 'react-native';

export function ProductRow({ item, onPress }) {
  return (
    <Pressable onPress={() => onPress(item)} style={{ flexDirection: 'row', gap: 12, padding: 12, borderRadius: 16, backgroundColor: '#fff', borderWidth: 1, borderColor: '#F3F4F6' }}>
      <Image source={{ uri: item.image }} style={{ width: 64, height: 64, borderRadius: 12 }} />
      <View style={{ flex: 1, justifyContent: 'center' }}>
        <Text style={{ fontWeight: '700', color: '#111827' }}>{item.title}</Text>
        <Text style={{ color: '#6B7280', marginTop: 4 }} numberOfLines={2}>{item.subtitle}</Text>
      </View>
      <View style={{ justifyContent: 'center' }}>
        <Text style={{ fontWeight: '700' }}>${item.price}</Text>
      </View>
    </Pressable>
  );
}

Práctica guiada: de diseño estático a pantalla real

Objetivo: transformar un diseño estático (tipo “Home de tienda”) en una pantalla funcional con secciones, estilos consistentes, un buscador y una lista de productos. Trabajaremos con: header, barra de búsqueda, categorías horizontales y listado vertical.

Paso 1: Define el layout general con ScrollView

Usa ScrollView para permitir desplazamiento de toda la pantalla y un contentContainerStyle con padding y separación.

import { ScrollView, StyleSheet } from 'react-native';

export function ShopHomeScreen() {
  return (
    <ScrollView contentContainerStyle={styles.container}>
      <HeaderSection />
      <SearchSection />
      <CategoriesSection />
      <ProductsSection />
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    padding: 16,
    gap: 16,
    backgroundColor: '#FFFFFF',
  },
});

Paso 2: Crea la sección Header (View + Text + Image + Pressable)

Incluye saludo, subtítulo y un avatar. Usa Flexbox en fila y flex: 1 para que el texto ocupe el espacio disponible.

import { View, Text, Image, Pressable, StyleSheet } from 'react-native';

function HeaderSection() {
  return (
    <View style={headerStyles.row}>
      <View style={{ flex: 1 }}>
        <Text style={headerStyles.title}>Explora</Text>
        <Text style={headerStyles.subtitle}>Encuentra productos para hoy</Text>
      </View>
      <Pressable style={headerStyles.avatarWrap}>
        <Image source={{ uri: 'https://picsum.photos/100' }} style={headerStyles.avatar} />
      </Pressable>
    </View>
  );
}

const headerStyles = StyleSheet.create({
  row: { flexDirection: 'row', alignItems: 'center', gap: 12 },
  title: { fontSize: 24, fontWeight: '800', color: '#111827' },
  subtitle: { marginTop: 4, color: '#6B7280' },
  avatarWrap: { width: 44, height: 44, borderRadius: 22, overflow: 'hidden' },
  avatar: { width: '100%', height: '100%' },
});

Paso 3: Sección de búsqueda (TextInput + icono “falso”)

Construye un campo de búsqueda con un contenedor tipo “pill”. El icono puede ser un Text o una View decorativa; lo importante es el layout.

import { useState } from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';

function SearchSection() {
  const [query, setQuery] = useState('');

  return (
    <View style={searchStyles.box}>
      <Text style={searchStyles.icon}>🔎</Text>
      <TextInput
        value={query}
        onChangeText={setQuery}
        placeholder="Buscar productos"
        style={searchStyles.input}
        autoCapitalize="none"
      />
    </View>
  );
}

const searchStyles = StyleSheet.create({
  box: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 10,
    paddingHorizontal: 12,
    paddingVertical: 10,
    borderRadius: 14,
    backgroundColor: '#F3F4F6',
  },
  icon: { fontSize: 16 },
  input: { flex: 1, fontSize: 16 },
});

Paso 4: Categorías horizontales (FlatList horizontal + Pressable)

Usa FlatList horizontal para categorías. Mantén un estado selected para resaltar la categoría activa.

import { useMemo, useState } from 'react';
import { View, Text, FlatList, Pressable, StyleSheet } from 'react-native';

function CategoriesSection() {
  const categories = useMemo(() => ['Todo', 'Novedades', 'Hogar', 'Tecnología', 'Ropa'], []);
  const [selected, setSelected] = useState('Todo');

  return (
    <View style={{ gap: 10 }}>
      <Text style={catStyles.heading}>Categorías</Text>
      <FlatList
        data={categories}
        keyExtractor={(c) => c}
        horizontal
        showsHorizontalScrollIndicator={false}
        contentContainerStyle={{ gap: 10 }}
        renderItem={({ item }) => {
          const active = item === selected;
          return (
            <Pressable
              onPress={() => setSelected(item)}
              style={[catStyles.pill, active ? catStyles.pillActive : catStyles.pillIdle]}
            >
              <Text style={[catStyles.pillText, active ? catStyles.pillTextActive : catStyles.pillTextIdle]}>
                {item}
              </Text>
            </Pressable>
          );
        }}
      />
    </View>
  );
}

const catStyles = StyleSheet.create({
  heading: { fontSize: 16, fontWeight: '800', color: '#111827' },
  pill: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: 999 },
  pillActive: { backgroundColor: '#111827' },
  pillIdle: { backgroundColor: '#F3F4F6' },
  pillText: { fontWeight: '700' },
  pillTextActive: { color: '#FFFFFF' },
  pillTextIdle: { color: '#111827' },
});

Paso 5: Listado de productos (FlatList vertical dentro de una sección)

En una pantalla con ScrollView, una lista grande puede generar conflictos de scroll. Para esta práctica, mantén el listado corto o renderiza productos como “bloques” dentro del ScrollView. Alternativa profesional: usar solo FlatList como contenedor principal y renderizar secciones con ListHeaderComponent. Aquí aplicaremos ese patrón para escalar mejor.

import { useMemo } from 'react';
import { FlatList, View, Text, StyleSheet } from 'react-native';
import { ProductRow } from './ProductRow';

export function ShopHomeScreen() {
  const products = useMemo(
    () => [
      { id: '1', title: 'Auriculares', subtitle: 'Sonido nítido y cancelación de ruido.', price: 59, image: 'https://picsum.photos/200?1' },
      { id: '2', title: 'Lámpara', subtitle: 'Luz cálida para tu escritorio.', price: 24, image: 'https://picsum.photos/200?2' },
      { id: '3', title: 'Mochila', subtitle: 'Resistente, ideal para el día a día.', price: 39, image: 'https://picsum.photos/200?3' },
    ],
    []
  );

  return (
    <FlatList
      data={products}
      keyExtractor={(p) => p.id}
      contentContainerStyle={styles.container}
      ItemSeparatorComponent={() => <View style={{ height: 12 }} />}
      ListHeaderComponent={
        <View style={{ gap: 16 }}>
          <HeaderSection />
          <SearchSection />
          <CategoriesSection />
          <Text style={styles.heading}>Recomendados</Text>
        </View>
      }
      renderItem={({ item }) => (
        <ProductRow item={item} onPress={() => {}} />
      )}
    />
  );
}

const styles = StyleSheet.create({
  container: { padding: 16, backgroundColor: '#FFFFFF' },
  heading: { fontSize: 16, fontWeight: '800', color: '#111827' },
});

Paso 6: Ajustes finos de layout (espaciado, alineación, densidad visual)

  • Si la UI se ve “apretada”, aumenta gap o separadores (ItemSeparatorComponent).
  • Si se ve “vacía”, reduce padding o usa tarjetas con borde sutil (borderColor claro).
  • Para alinear textos y precios, usa justifyContent: 'center' en columnas pequeñas y flex: 1 en el bloque principal.
  • Para consistencia, centraliza colores y tamaños en un archivo de constantes (por ejemplo theme.js) y reutilízalos en StyleSheet.

Paso 7: Checklist de calidad visual

  • ¿Los textos tienen jerarquía clara (título vs subtítulo vs cuerpo)?
  • ¿Los botones tienen estado presionado (feedback)?
  • ¿Las imágenes tienen tamaño consistente y bordes redondeados sin deformarse (resizeMode)?
  • ¿La lista mantiene rendimiento (usar FlatList para colecciones)?
  • ¿Los espaciados siguen una escala (8/12/16)?

Ahora responde el ejercicio sobre el contenido:

¿Qué enfoque es más adecuado para construir una pantalla con secciones y un listado que pueda crecer sin problemas de rendimiento y scroll?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Para una UI con secciones y una lista que puede crecer, FlatList es preferible porque virtualiza filas. Al usar ListHeaderComponent puedes incluir header, búsqueda y categorías sin mezclar dos scrolls principales.

Siguiente capítulo

Navegación profesional en React Native

Arrow Right Icon
Portada de libro electrónico gratuitaReact Native desde Cero a App Profesional
17%

React Native desde Cero a App Profesional

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.