Práctica: Crear una app de lista de tareas con navegación

Lectura
30 min~11 min lectura

Práctica: Crear una app de lista de tareas con navegación

En esta lección práctica, consolidarás los conocimientos fundamentales de React Native y Expo integrando dos pilares esenciales para cualquier aplicación de mediana a gran complejidad: la navegación y la gestión de estado. Construiremos una aplicación de lista de tareas (To-Do) funcional que no solo permita crear y eliminar tareas, sino que también implemente un sistema de navegación por pestañas (Tab Navigation) y una pantalla de detalle para cada tarea. Este proyecto servirá como un microcosmos de las decisiones arquitectónicas que enfrentarás en el desarrollo profesional de apps móviles.

Asumiremos que tienes un entorno de Expo configurado y conocimientos básicos de componentes React Native, JSX y hooks como useState. El objetivo final es producir una aplicación estructurada, con un estado global accesible desde diferentes pantallas y una experiencia de usuario fluida gracias a una navegación intuitiva. Utilizaremos React Navigation para el enrutamiento y nos centraremos en un patrón de gestión de estado basado en React Context para mantener la simplicidad y la didáctica, aunque mencionaremos alternativas más robustas para proyectos mayores.

Arquitectura de la Aplicación y Configuración Inicial

Antes de escribir la primera línea de código, es crucial planificar la estructura de nuestra aplicación. Definiremos tres pantallas principales: una pantalla de lista (Home), donde el usuario verá todas sus tareas y podrá agregar nuevas; una pantalla de detalle (Detail), donde se podrá ver la descripción completa de una tarea y quizás editarla en el futuro; y una pantalla de perfil (Profile), un espacio simulado para opciones de usuario que nos permitirá demostrar la navegación por pestañas. La navegación será híbrida: un Tab Navigator contendrá las pantallas Home y Profile, y desde la Home podremos navegar a la pantalla de Detail mediante un Stack Navigator anidado.

Inicia creando un nuevo proyecto Expo: expo init TodoAppWithNavigation. Selecciona la plantilla "blank (TypeScript)" para tener un mejor control de tipos, aunque usaremos JavaScript en los ejemplos por claridad. Luego, instala las dependencias esenciales de navegación: npm install @react-navigation/native @react-navigation/bottom-tabs @react-navigation/stack. Además, instala las dependencias de Expo requeridas por React Navigation: expo install react-native-screens react-native-safe-area-context. Esta configuración proporciona la base para construir tanto navegadores de pila como de pestañas con un rendimiento nativo.

Tip: Organiza tu código por características (feature-based) desde el principio. En lugar de tener una carpeta gigante de "screens", considera agrupar los archivos relacionados (pantalla, componentes específicos, lógica de estado) en carpetas como "features/todo", "features/profile". Esto escala mucho mejor.

Concepto Clave: Navegación Anidada y Contexto de Estado

Imagina un centro comercial. El plano principal del centro (la Tab Navigation) te muestra las grandes secciones: Ropa, Electrónica, Alimentación. Dentro de la sección de "Ropa" (que sería nuestra pantalla Home), te mueves entre diferentes tiendas (Zara, H&M) utilizando pasillos. Este movimiento dentro de una sección es análogo a un Stack Navigator anidado: desde la lista de tiendas (lista de tareas) puedes entrar a una tienda específica (detalle de una tarea), y siempre puedes volver atrás (con el botón físico o el gesto) sin salir de la sección de Ropa. La navegación anidada permite esta jerarquía lógica y espacial en tu app.

Por otro lado, el Contexto de React actúa como el sistema de megafonía del centro comercial. Cuando una tienda anuncia una liquidación (un cambio de estado), el mensaje llega a todos los clientes en esa sección (componentes suscritos) sin necesidad de que cada tienda le pase un cartel manualmente a cada persona (prop drilling). En nuestra app, el contexto contendrá la lista global de tareas y las funciones para modificarla (añadir, eliminar, marcar como completada). Cualquier pantalla o componente, ya sea la Home, la Detail o un componente de botón dentro de ellas, podrá "escuchar" este estado y despachar acciones para cambiarlo, manteniendo la sincronización de la UI en todo momento.

Cómo Funciona en la Práctica: Implementando el Contexto y el Navegador Principal

Comencemos por el corazón de la aplicación: el contexto de estado. Crearemos un archivo llamado TodoContext.js. Aquí, definiremos un contexto que proporcione un array de tareas (cada una con id, título, descripción y estado de completado) y funciones para agregar y eliminar tareas. Utilizaremos el hook useReducer en lugar de useState porque maneja lógica de estado compleja de forma más predecible, especialmente cuando las actualizaciones dependen del estado anterior o involucran múltiples subvalores. El reducer será una función pura que, dado el estado actual y una "acción" (como 'ADD_TODO'), devuelve el nuevo estado.

Una vez definido el contexto, debemos envolver nuestro árbol de componentes con el Provider que exporta. El lugar ideal para hacerlo es en el punto más alto de la aplicación, típicamente en el archivo App.js. Justo después del Provider, configuraremos nuestro navegador raíz. En nuestro caso, será un Bottom Tab Navigator que contenga dos pantallas: la pantalla Home (que a su vez tendrá su propio Stack Navigator anidado) y la pantalla Profile. Esta estructura asegura que las pestañas sean visibles en la pantalla Home y en la Detail (ya que Detail es parte del stack de Home), pero no en la pantalla Profile si así lo decidimos.


// App.js - Estructura principal
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { TodoProvider } from './context/TodoContext';
import HomeStackNavigator from './navigation/HomeStackNavigator';
import ProfileScreen from './screens/ProfileScreen';

const Tab = createBottomTabNavigator();

export default function App() {
  return (
    <TodoProvider>
      <NavigationContainer>
        <Tab.Navigator screenOptions={{ headerShown: false }}>
          <Tab.Screen name="Inicio" component={HomeStackNavigator} />
          <Tab.Screen name="Perfil" component={ProfileScreen} />
        </Tab.Navigator>
      </NavigationContainer>
    </TodoProvider>
  );
}

Código en Acción: Pantalla de Lista, Detalle y Lógica de Contexto

Ahora profundicemos en la implementación concreta. Primero, veamos el reducer y el contexto. El reducer manejará las acciones 'ADD_TODO', 'TOGGLE_TODO' y 'DELETE_TODO'. El contexto expondrá el estado 'todos' y la función 'dispatch' para que los componentes puedan enviar estas acciones. Luego, en la pantalla Home (HomeScreen.js), usaremos useContext para acceder a la lista de tareas y al dispatch. Renderizaremos las tareas en un FlatList y cada elemento será un Pressable que al tocar navegue a la pantalla de Detail, pasando el ID de la tarea como parámetro de ruta.

La pantalla de detalle (DetailScreen.js) recibirá el ID de la tarea a través de sus props de ruta (route.params). Usando este ID, buscará la tarea correspondiente dentro de la lista obtenida del contexto. Mostrará el título y la descripción, y tendrá un botón para eliminar la tarea. Al eliminar, despachará la acción 'DELETE_TODO' y luego navegará de regreso a la lista. La navegación se realiza mediante el hook useNavigation proporcionado por React Navigation.


// context/TodoContext.js
import React, { createContext, useReducer, useContext } from 'react';

const TodoContext = createContext();

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, {
        id: Date.now().toString(),
        title: action.payload.title,
        description: action.payload.description,
        completed: false
      }];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
};

export const TodoProvider = ({ children }) => {
  const [todos, dispatch] = useReducer(todoReducer, []);

  return (
    <TodoContext.Provider value={{ todos, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
};

export const useTodos = () => {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodos debe usarse dentro de un TodoProvider');
  }
  return context;
};

// screens/HomeScreen.js
import React, { useState } from 'react';
import { View, FlatList, TextInput, Button, Pressable, Text } from 'react-native';
import { useTodos } from '../context/TodoContext';
import { useNavigation } from '@react-navigation/native';

const HomeScreen = () => {
  const { todos, dispatch } = useTodos();
  const navigation = useNavigation();
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');

  const handleAddTodo = () => {
    if (title.trim()) {
      dispatch({ type: 'ADD_TODO', payload: { title, description } });
      setTitle('');
      setDescription('');
    }
  };

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <TextInput placeholder="Título" value={title} onChangeText={setTitle} />
      <TextInput placeholder="Descripción" value={description} onChangeText={setDescription} />
      <Button title="Agregar Tarea" onPress={handleAddTodo} />

      <FlatList
        data={todos}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <Pressable
            onPress={() => navigation.navigate('Detail', { todoId: item.id })}
            style={{ padding: 15, borderBottomWidth: 1 }}
          >
            <Text style={{ fontWeight: 'bold' }}>{item.title}</Text>
            <Text>{item.completed ? '✅ Completada' : '⏳ Pendiente'}</Text>
          </Pressable>
        )}
      />
    </View>
  );
};

export default HomeScreen;

// screens/DetailScreen.js
import React from 'react';
import { View, Text, Button, Alert } from 'react-native';
import { useTodos } from '../context/TodoContext';
import { useNavigation, useRoute } from '@react-navigation/native';

const DetailScreen = () => {
  const route = useRoute();
  const navigation = useNavigation();
  const { todoId } = route.params;
  const { todos, dispatch } = useTodos();

  const todo = todos.find(t => t.id === todoId);

  const handleDelete = () => {
    Alert.alert(
      "Eliminar Tarea",
      "¿Estás seguro de que quieres eliminar esta tarea?",
      [
        { text: "Cancelar", style: "cancel" },
        {
          text: "Eliminar",
          style: "destructive",
          onPress: () => {
            dispatch({ type: 'DELETE_TODO', payload: todoId });
            navigation.goBack();
          }
        }
      ]
    );
  };

  if (!todo) {
    return (
      <View>
        <Text>Tarea no encontrada</Text>
      </View>
    );
  }

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text style={{ fontSize: 24, fontWeight: 'bold' }}>{todo.title}</Text>
      <Text style={{ fontSize: 16, marginVertical: 10 }}>{todo.description}</Text>
      <Text>Estado: {todo.completed ? 'Completada' : 'Pendiente'}</Text>
      <Button title="Eliminar Tarea" color="red" onPress={handleDelete} />
    </View>
  );
};

export default DetailScreen;

Errores Comunes y Cómo Evitarlos

1. Mutar el estado directamente en el contexto: Nunca modifiques el array 'todos' o un objeto 'todo' directamente (ej: todos.push(newTodo)). Siempre devuelve un nuevo estado desde el reducer. Usa operadores de propagación (...state) o métodos inmutables como map y filter. La mutación directa no provocará una re-renderización de los componentes suscritos y llevará a inconsistencias en la UI.

2. Pasar funciones complejas por parámetros de navegación: Los parámetros de ruta (route.params) deben contener solo datos serializables simples (strings, números, objetos planos). Nunca pases funciones, componentes o instancias de clase. Para compartir lógica entre pantallas, utiliza el contexto global o callbacks definidos en el contexto. Pasar una función directamente puede parecer que funciona, pero fallará al recargar la pantalla o en ciertas condiciones de rendimiento.

3. No manejar el caso de parámetros undefined en la pantalla de detalle: Si un usuario accede directamente a la URL/Deep Link o si hay un error de navegación, 'route.params.todoId' podría ser undefined. Siempre implementa una comprobación defensiva, como se muestra en el código de DetailScreen, para evitar crashes mostrando un mensaje amigable o redirigiendo al usuario.

4. Anidar navegadores de forma incorrecta causando dobles headers o comportamientos extraños: Un error típico es envolver una pantalla que ya es un Stack.Navigator con otro header. Recuerda: el headerShown: false es tu amigo. Si usas un Stack Navigator anidado dentro de un Tab, probablemente quieras ocultar el header del Tab Navigator (como hicimos en App.js) y manejar los headers dentro de cada Stack individualmente para un mayor control.

5. Olvidar envolver la app con NavigationContainer: Es el componente de nivel superior que gestiona el árbol de navegación. Sin él, los hooks useNavigation y useRoute no funcionarán y tu app lanzará un error. Asegúrate de que esté presente y sea un ancestro directo de todos tus navegadores.

Checklist de Dominio

Antes de considerar esta lección completa, verifica que puedes realizar o comprender cada uno de los siguientes puntos:

  • Configurar un nuevo proyecto Expo e instalar las dependencias necesarias para React Navigation (native, stack, bottom-tabs).
  • Crear un Contexto de React que utilice useReducer para manejar un estado complejo (como una lista de tareas con acciones de añadir, eliminar y modificar).
  • Implementar un navegador híbrido: un Bottom Tab Navigator como raíz, con al menos un Stack Navigator anidado en una de las pestañas.
  • Navegar de una pantalla a otra (ej: de Home a Detail) pasando parámetros (como un ID) y recibirlos correctamente en la pantalla destino usando useRoute.
  • Desde cualquier pantalla (Home, Detail), despachar acciones al contexto global para modificar el estado y observar cómo la UI se actualiza automáticamente en todas las pantallas afectadas.
  • Manejar la navegación hacia atrás (goBack) programáticamente después de una acción, como eliminar un ítem.
  • Implementar una comprobación de seguridad en la pantalla de detalle para manejar el caso en que el parámetro de ruta sea inválido o no exista.
  • Explicar la diferencia entre navegación por pestañas (para secciones principales de la app) y navegación por pila (para flujos dentro de una sección).
De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión