Gestor de Tareas
Realizado durante el año 2022
Lo que comenzó como una lista de tareas (To-Do List) estándar, evolucionó rápidamente hacia algo más ambicioso. Me di cuenta de que una lista lineal no refleja cómo trabajamos realmente; necesitamos estados, procesos y movimiento.
El objetivo cambió: construir un Tablero Kanban completo, donde el usuario pudiera crear columnas dinámicas (“Backlog”, “En Desarrollo”, “QA”, “Terminado”) y mover tareas entre ellas con total libertad.
1. Estructura de Datos
El primer obstáculo fue rediseñar el estado. Ya no servía un simple array de tareas [task1, task2]. Necesitaba una estructura relacional normalizada para evitar duplicidad y facilitar el renderizado.
Opté por separar las tareas de las columnas, manteniendo un orden específico mediante arrays de IDs. Esto permite que cambiar el orden sea barato computacionalmente (solo muevo un string en un array) en lugar de mutar objetos pesados.
const initialData = {
tasks: {
'task-1': { id: 'task-1', content: 'Configurar Webpack' },
'task-2': { id: 'task-2', content: 'Diseñar componentes' },
},
columns: {
'column-1': {
id: 'column-1',
title: 'Por hacer',
taskIds: ['task-1', 'task-2'], // El orden vive aquí
},
},
columnOrder: ['column-1'], // El orden de las columnas
};
2. Drag & Drop
Implementar el arrastre visual es solo la punta del iceberg. El verdadero reto fue la persistencia lógica de ese movimiento.
Cuando soltamos una tarjeta (onDragEnd), hay dos escenarios posibles que el código debe manejar con precisión quirúrgica para evitar que la UI “parpadee” o los datos se corrompan:
Escenario A: Reordenar en la misma columna Es una operación de array simple. Sacar el elemento de su índice original e insertarlo en el nuevo.
Escenario B: Mover entre columnas diferentes
Aquí la complejidad aumenta. Debo eliminar el ID de la columna source, agregarlo a la columna destination, y actualizar el estado de ambas columnas simultáneamente.
const onDragEnd = (result) => {
const { destination, source, draggableId } = result;
// Si el usuario suelta fuera de una zona válida, no hacemos nada
if (!destination) return;
// Si movió la tarea a otra columna
if (start !== finish) {
const startTaskIds = Array.from(start.taskIds);
startTaskIds.splice(source.index, 1); // Sacar de origen
const finishTaskIds = Array.from(finish.taskIds);
finishTaskIds.splice(destination.index, 0, draggableId); // Poner en destino
const newState = {
...state,
columns: {
...state.columns,
[newStart.id]: newStart,
[newFinish.id]: newFinish,
},
};
setState(newState); // Actualización atómica
}
};
3. Persistencia
Un problema común en estas apps es que, al soltar la tarjeta, esta vuelve a su lugar original hasta que la base de datos confirma el cambio. Eso rompe la inmersión.
Implementé una actualización optimista: el estado local de React se actualiza instantáneamente para que el usuario sienta la respuesta inmediata. La persistencia (guardado en LocalStorage o base de datos) ocurre en segundo plano. Si recargo la página, la lógica de inicialización reconstruye el tablero exactamente como lo dejé, respetando índices y columnas creadas.
Conclusión: Este proyecto me enseñó que una buena UX (como arrastrar una tarjeta) depende enteramente de una estructura de datos robusta y eficiente en el backend del frontend.
Tecnologías:
- React (Hooks & State)
- Drag and Drop (Lógica de reordenamiento)
- Styled Components (Para estilos dinámicos según el estado del arrastre)