Práctica: Crea 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: la navegación y la gestión de estado. Construirás una aplicación de lista de tareas completamente funcional que no solo permitirá agregar y visualizar tareas, sino que también implementará una navegación profesional entre pantallas. Esta aplicación servirá como un proyecto de referencia que demuestra cómo estructurar una app móvil real, separando la lógica de presentación, la navegación y el estado de la aplicación de manera clara y mantenible.
Partiremos de un proyecto Expo inicial y, paso a paso, integraremos React Navigation para crear un stack de navegación con dos pantallas principales. Utilizaremos el estado de React, específicamente el hook useState, para manejar nuestra lista de tareas, y pasaremos datos entre pantallas mediante los parámetros de navegación. Al final de esta lección, tendrás una aplicación que puedes ejecutar en tu dispositivo físico o emulador, comprendiendo profundamente el flujo de datos y la interacción entre componentes en una arquitectura con múltiples vistas.
Configuración del Proyecto y Estructura de Navegación
Antes de escribir código de negocio, es crucial establecer una base sólida. Crearemos un nuevo proyecto Expo e instalaremos las dependencias necesarias para la navegación. React Navigation es la biblioteca estándar de facto para la navegación en React Native, y para una configuración simple utilizaremos el paquete @react-navigation/native junto con @react-navigation/stack. Además, necesitaremos instalar las dependencias compatibles con el entorno Expo, como react-native-screens y react-native-safe-area-context. Este setup inicial garantiza que tengamos todas las herramientas listas para construir un flujo de pantallas coherente.
La estructura de navegación que implementaremos será un Stack Navigator. Imagina este navegador como una pila de papeles: la pantalla principal está en la base, y cuando navegas a una nueva pantalla, se coloca una nueva hoja encima. Para regresar, simplemente retiras la hoja superior, revelando la anterior. En nuestra app, tendremos una pantalla principal (HomeScreen) que muestra la lista de tareas, y una pantalla de detalle o creación (TaskDetailScreen) a la que se accederá al tocar una tarea existente o un botón para agregar una nueva. Configurar este navigator es el primer paso para definir el esqueleto de nuestra aplicación.
# Crear un nuevo proyecto Expo
npx create-expo-app TodoAppWithNavigation
cd TodoAppWithNavigation
# Instalar las dependencias de navegación
npx expo install @react-navigation/native @react-navigation/stack react-native-screens react-native-safe-area-context
# Para manejar gestos (necesario para Stack Navigator en Android)
npx expo install react-native-gesture-handler
Concepto Clave: Estado, Navegación y Props
Comprender la relación entre el estado, la navegación y las props es fundamental para cualquier desarrollo en React Native. El estado es la memoria de tu componente. En nuestra app, la lista de tareas vive en el estado de un componente (por ejemplo, HomeScreen). La navegación es el mecanismo que permite transitar entre diferentes componentes (pantallas). Las props son la forma de pasar información de un componente a otro, y la navegación utiliza este concepto: cuando navegas a una pantalla, puedes pasarle parámetros (props) a través del objeto de navegación.
Una analogía del mundo real sería un restaurante. El estado es el menú completo y el estado de las mesas (ocupadas o libres). El maitre (el navegador) es quien te guía a tu mesa (pantalla). Cuando el maitre te lleva a tu mesa, puede pasarte información (props), como "esta mesa es para no fumadores" o "aquí está la carta especial del día". De manera similar, al navegar de la lista de tareas a la pantalla de detalle, pasamos el objeto de la tarea específica como un parámetro para que la pantalla de detalle sepa exactamente qué tarea debe mostrar o editar.
Tip: En React Navigation, los parámetros que pasas entre pantallas se agrupan en un objeto llamado route.params. Siempre debes verificar si un parámetro existe antes de intentar acceder a él, usando un operador de encadenamiento opcional (?. ) o una verificación condicional, para evitar errores de ejecución.
Cómo Funciona en la Práctica: Flujo de Datos Paso a Paso
Vamos a desglosar el flujo completo de la aplicación en una secuencia lógica. Primero, en la HomeScreen, el usuario ve una lista de tareas (inicialmente vacía) y un botón flotante para agregar una nueva. Este componente mantiene en su estado local (usando useState) un array de objetos, donde cada objeto representa una tarea con propiedades como id, title, description y completed. Cuando el usuario presiona el botón "Agregar", se dispara una función de navegación (navigation.navigate) que lleva al usuario a la TaskDetailScreen, pero esta vez sin pasar parámetros, indicando que es un modo de creación.
En la TaskDetailScreen, el usuario llena un formulario con el título y la descripción de la tarea. Al presionar "Guardar", la pantalla de detalle debe comunicar esta nueva información de vuelta a la HomeScreen. Aquí hay un patrón clave: en lugar de intentar pasar datos directamente "hacia atrás", utilizamos la función navigation.setParams o, más comúnmente, pasamos una función de callback como parámetro durante la navegación inicial, o utilizamos un sistema de gestión de estado global (que veremos en lecciones posteriores). Para esta práctica, usaremos un enfoque práctico donde la pantalla de detalle actualiza los parámetros de la ruta, y la HomeScreen escucha estos cambios mediante el hook useFocusEffect para actualizar su lista. Este flujo bidireccional es el corazón de la comunicación entre pantallas.
Código en Acción: Implementación Completa de la App
A continuación, presentamos el código central de nuestra aplicación. Comenzamos configurando el Stack Navigator en nuestro archivo principal App.js. Luego, definimos el componente HomeScreen que contiene la lista y la lógica de estado. Finalmente, creamos el componente TaskDetailScreen que maneja la creación y edición. Presta atención a cómo se inicializa la navegación, cómo se pasa el objeto de navegación (navigation) y ruta (route) a cada pantalla, y cómo se manipula el estado.
// App.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from './screens/HomeScreen';
import TaskDetailScreen from './screens/TaskDetailScreen';
const Stack = createStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'Mis Tareas' }}
/>
<Stack.Screen
name="TaskDetail"
component={TaskDetailScreen}
options={{ title: 'Detalle de Tarea' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
Ahora, el componente de la pantalla principal, HomeScreen. Este componente gestiona la lista de tareas y proporciona la interfaz para navegar a la pantalla de detalle.
// screens/HomeScreen.js
import React, { useState, useCallback } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
Button,
} from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
const HomeScreen = ({ navigation }) => {
const [tasks, setTasks] = useState([]);
// Función para agregar o actualizar una tarea
const handleSaveTask = (task) => {
if (task.id) {
// Actualizar tarea existente
setTasks((prevTasks) =>
prevTasks.map((t) => (t.id === task.id ? task : t))
);
} else {
// Agregar nueva tarea
const newTask = { ...task, id: Date.now().toString() };
setTasks((prevTasks) => [...prevTasks, newTask]);
}
};
// Efecto para recargar si se recibe data de TaskDetail (enfoque alternativo)
useFocusEffect(
useCallback(() => {
// Aquí podrías recargar datos de una API o estado global
// Por simplicidad, mantenemos el estado local.
}, [])
);
const renderItem = ({ item }) => (
<TouchableOpacity
style={styles.taskItem}
=> navigation.navigate('TaskDetail', { task: item, onSave: handleSaveTask })}
>
<Text style={styles.taskTitle}>{item.title}</Text>
<Text style={styles.taskStatus}>
{item.completed ? 'Completada' : 'Pendiente'}
</Text>
</TouchableOpacity>
);
return (
<View style={styles.container}>
<FlatList
data={tasks}
renderItem={renderItem}
keyExtractor={(item) => item.id}
ListEmptyComponent={<Text style={styles.emptyText}>No hay tareas. ¡Agrega una!</Text>}
/>
<Button
title="Agregar Nueva Tarea"
=> navigation.navigate('TaskDetail', { onSave: handleSaveTask })}
/>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
taskItem: {
backgroundColor: '#f9f9f9',
padding: 16,
marginVertical: 8,
borderRadius: 8,
borderWidth: 1,
borderColor: '#ddd',
},
taskTitle: { fontSize: 18, fontWeight: 'bold' },
taskStatus: { fontSize: 14, color: 'gray', marginTop: 4 },
emptyText: { textAlign: 'center', marginTop: 50, fontSize: 16, color: '#999' },
});
export default HomeScreen;
Finalmente, la pantalla de detalle, TaskDetailScreen, que maneja tanto la creación como la edición basándose en los parámetros recibidos.
// screens/TaskDetailScreen.js
import React, { useState } from 'react';
import { View, TextInput, Button, StyleSheet, Switch, Text } from 'react-native';
const TaskDetailScreen = ({ navigation, route }) => {
// Recibimos la tarea existente y la función onSave desde los parámetros de navegación
const { task: existingTask, onSave } = route.params || {};
const [title, setTitle] = useState(existingTask?.title || '');
const [description, setDescription] = useState(existingTask?.description || '');
const [completed, setCompleted] = useState(existingTask?.completed || false);
const handleSave = () => {
if (!title.trim()) {
alert('El título es obligatorio');
return;
}
const taskData = {
id: existingTask?.id, // Si existe, mantenemos el ID para actualizar
title: title.trim(),
description: description.trim(),
completed,
};
// Llamamos a la función onSave pasada desde HomeScreen
if (onSave) {
onSave(taskData);
}
// Navegamos de regreso a la pantalla principal
navigation.goBack();
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="Título de la tarea"
value={title}
/>
<TextInput
style={[styles.input, styles.textArea]}
placeholder="Descripción (opcional)"
value={description}
multiline
numberOfLines={4}
/>
<View style={styles.switchContainer}>
<Text>Completada:</Text>
<Switch
value={completed}
/>
</View>
<Button title="Guardar Tarea" />
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
marginBottom: 16,
fontSize: 16,
},
textArea: {
height: 100,
textAlignVertical: 'top',
},
switchContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 24,
},
});
export default TaskDetailScreen;
Errores Comunes y Cómo Evitarlos
Al integrar navegación y estado, es fácil caer en ciertos patrones problemáticos. Aquí describimos los errores más frecuentes y sus soluciones:
1. Mutar el Estado Directamente: Nunca modifiques el array de tareas directamente (ej: tasks.push(newTask)). React no podrá detectar el cambio. Siempre usa la función setter del estado (setTasks) con un nuevo array o un nuevo objeto, aprovechando el patrón de actualización funcional cuando el nuevo estado depende del anterior.
2. No Verificar Parámetros de Navegación: Asumir que route.params.task o route.params.onSave siempre existen causará errores. Siempre usa encadenamiento opcional (existingTask?.title) o verifica con una condición antes de acceder a propiedades anidadas, especialmente en pantallas a las que se puede navegar de múltiples maneras (creación vs. edición).
3. Pasar Funciones no Memorizadas como Parámetros: Si defines una función como handleSaveTask dentro de HomeScreen sin usar useCallback y la pasas como parámetro de navegación, se creará una nueva referencia en cada renderizado. Esto puede provocar renders innecesarios en la pantalla de detalle. Envolver la función en useCallback con las dependencias correctas optimiza este comportamiento.
4. Olvidar el keyExtractor en FlatList: Si tu lista de tareas no tiene una propiedad key única o no proporcionas la prop keyExtractor a la FlatList, React Native no podrá identificar eficientemente los elementos durante las actualizaciones, lo que lleva a comportamientos erráticos y pérdida de estado de ítem (como el estado de un Switch dentro de un ítem de la lista). Asegúrate de que cada tarea tenga un id único y estable.
5. Manejo Inadecuado del "Go Back": Confiar únicamente en navigation.goBack() sin haber actualizado el estado en la pantalla anterior puede dejar la UI desincronizada. En nuestro ejemplo, llamamos a onSave antes de hacer goBack(). En aplicaciones más complejas, considera usar eventos de navegación (como navigation.addListener antes del desenfoque) o un estado global para garantizar la consistencia de los datos.
Checklist de Dominio
Para verificar que has comprendido y puedes replicar los conceptos de esta lección, asegúrate de poder realizar y explicar cada uno de los siguientes puntos:
- Crear un nuevo proyecto Expo e instalar correctamente las dependencias de React Navigation (Stack Navigator).
- Configurar un NavigationContainer y un Stack.Navigator con al menos dos pantallas en el archivo principal.
- Utilizar el hook useState para gestionar una lista de objetos (tareas) en un componente pantalla.
- Navegar de una pantalla a otra utilizando navigation.navigate('ScreenName', params), pasando tanto datos (un objeto tarea) como funciones callback (onSave).
- En la pantalla de destino, acceder a los parámetros pasados mediante route.params y manejar condicionalmente los modos de "creación" y "edición".
- Actualizar el estado de la pantalla principal (lista de tareas) con la información proveniente de la pantalla de detalle, ya sea para agregar un nuevo ítem o para editar uno existente.
- Implementar una FlatList para renderizar la lista de tareas de manera eficiente, con un keyExtractor adecuado y un componente para la lista vacía.
- Estilizar los componentes básicos (View, Text, TextInput, Button, TouchableOpacity) para crear una interfaz de usuario clara y funcional.