Priority Time
Hola, vamos a empezar la serie, con unos conceptos básicos de react, en los que no nos vamos a detener mucho, solo los vamos a mencionar, pero son importantes, y luego nos vamos a ir a unas estructuras de datos y como manipularlas para realizar diferentes operaciones.
Para esto nos vamos a enfocar en un proyecto que hice para auxiliarme. Les dejo el enlace del repo y el demo para que puedan probarlo.
Empezamos con un poco de contexto.
Idea
Hacer una app To-Do Tasks (esta la piden un mucho en pruebas técnicas), pero vamos a agregarle un sistema de prioridad que nos permitirá visualizar las tareas de forma ascendente, además de guardar el estado en localStorage para darle persistencia a la app.
Diseño de la app
Es una SPA (single page application), la haremos con Next (client side), porque es fácil, a Vercel le encanta para desplegar y a mi también, pero es un proyecto tan simple que funciona copiando y pegando en una app React.
Componentes? Sí, pero solo dos para darle un poco de orden. Es un ejemplo de conceptos, no algo que veríamos en código de producción.
Estilos: Por eso también elegiremos Next. Tiene una de las formas más fáciles de empezar proyectos, podemos crear un template con Tailwind incluido y comenzar de inmediato a crear estilos.
Vamos a usar un par de hooks de React, el de estado y el de efectos; o sea useState() y useEffect(), luego ponemos el porque, se puede hacer más complejo con Redux, React-forms… sí, pero vamos simple, hooks de React y localStorage.
El layout será sencillo, un bloque main para contener un formulario y dos listas, usaremos una mezcla de grid y flex, nada del otro mundo.
Preámbulo
Empecemos desmenuzando un poco el código para ir entendiendo cómo se construye. Recuerden, esta aquí.
"use client";
import { ChangeEvent, FormEvent, useEffect, useState } from "react";Solo una cosa que comentar acá, en Next tenemos componentes que se ejecutan en el servidor(SSR) y nos regresa HTML listo para usarse, pero ese tipo de componentes regresan estáticos, o sea, no pueden tener ciclo de vida como lo que se ejecuta en el cliente por eso especificamos ‘use client’.
type TTask = {
id: string;
text: string;
priority: 0 | 1 | 2 | 3;
completed: boolean;
};Usamos TypeScript para tener un tipado fuerte, nos permite tener seguridad y certeza en el manejo de datos, autocompletado, ¿Por qué alguien no lo usuaria?, es genial.
Regresando al punto, Definimos TTask que es la estructura de una tarea, nuestra unidad atómica para trabajar.
type TTaskElement = TTask & {
onCheck: (id: string) => void;
onRemove: (id: string) => void;
};Este type vamos a ignorarlo de momento, luego lo retomamos.
const priorityMap = {
0: { copy: "Urgent", styles: "border-rose-500" },
1: { copy: "Important", styles: "border-orange-500" },
2: { copy: "Next week", styles: "border-green-500" },
3: { copy: "Not important", styles: "border-indigo-500" },
};Un diccionario(o hashmap en otros lenguajes), lo vamos a usar más adelante, pero hay que grabarse la estructura, me gusta mucho usarlos porque tenemos llaves numéricas que podemos usar como índices, y un objeto asignado que lo podemos usar para renderizar dinámicamente partes de los componentes, en este caso lo usaremos para asignar un color de borde y un texto a cada nivel de prioridad.
Componentes
const TaskItem = ({
text,
priority,
completed,
id,
onCheck,
onRemove,
}: TTaskElement) => (
<li
className={`flex h-10 bg-zinc-900 text-zinc-200 justify-between
items-center p-2 ${priorityMap[priority].styles} border rounded-lg`}
>
<div className={`flex items-center ${completed ? "line-through":"" }`}>
<input
className="mx-2"
type="checkbox"
checked={completed}
onChange={() => {
onCheck(id);
}}
/>
{text}
</div>
<button
className="cursor-pointer hover:opacity-70"
onClick={() => {
onRemove(id);
}}
>
❌
</button>
</li>
);Comenzamos con los componentes, este perfectamente podría ir en otro archivo, pero para propósitos del post acá esta perfecto. Haremos esta abstracción de lógica para evitar duplicar código que usamos en las listas. Este es un TaskList, tiene como props elementos de la tarea(TTask) y recuerdan TTaskElement que mencionamos anteriormente, para esto lo creamos, la idea es extender el uso de TTask con los métodos que usará este nuevo componente y que TS nos siga auxiliando.
El componente es sencillo, es un list item que contiene:
- Checkbox, que controla cuando una tarea es completada, tiene la función de regresar a “pending” si se vuelve a clicar, funciona con un método heredado onCheck.
- Texto, simplemente muestra la tarea en curso.
- Botón, éste nos permite disparar el método onRemove, nos permite eliminar la tarea correspondiente al id asignado.
const createId = () => {
return (Math.random() + 1).toString(36).substring(2, 10);
};Trucazo, una función que nos permite generar ids alfanuméricos de 8 posiciones para este ejemplo. Nota: Esto no se hace en producción, Math.random no puede asegurar que no exista duplicidad, para una entrevista o ejercicios está bien.
Get Marco Sandoval’s stories in your inbox
Join Medium for free to get updates from this writer.
Paréntesis. Vale la pena explicarlo.
(Math.random()+1) // Genera un número aleatorio entre 0 y 1, más 1
// queda algo como 1.3404189...
(Math.random() + 1).toString(36) // Nos regresa una representación
// del número 1.3404189... en base 36
// regresa '1.c96l4c7i4j'
(Math.random() + 1).toString(36).substring(2, 10) // Por último
// .substring(2,10)
// regresa 8 dígitos
// saltandose el "1."
// listo, tenemos un
// id="c96l4c7i"Manejo del estado
Entramos al componente principal
// Objeto TTask inicial.
const defaultTask: TTask = {
text: "",
id: "",
priority: 3,
completed: false,
};
const [task, setTask] = useState<TTask>(defaultTask);
// getter,setter que usaremos para el formulario, inicializamos con defaultTask
const [tasks, setTasks] = useState<TTask[]>([]);
// getter,setter para la lista de tareas.
// Comenzamos con los handlers
// Este es método genérico que utiliza el prop "name" de los inputs y select
// para definir su llave, la parte importante esta dentro del setTaks
// por inmutabilidad no podemos modificar tasks, entonces creamos y asignamos
// usando los valores previos y la condicional es para mantener el tipado
// cuando el name === "priority", ya que los inputs siempre nos regresarán
// strings en value
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setTask((prevTask) => ({
...prevTask,
[name]: name === "priority" ? Number(value) : value,
}));
};
// Este handler es sencillo, crea y asigna un nuevo elemento a tasks
// con el contenido de task y regresa el formulario a default
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const id = createId();
setTasks((prevTasks) => [...prevTasks, { ...task, id }]);
setTask(defaultTask);
};
// Estos son los mas interesantes, recuerden que mandamos este método por props
// a TaskItem, necesitamos que ejecute esta lógica usando el estado del padre,
// lo interesante aquí es que para generar el nuevo valor de tasks necesitamos
// buscar con el id la tarea que vamos a completar/descompletar, como tenemos un
// booleano un pequeño truco es negar su valor actual, los que no coincidan se
// guardan tal como son.
const handleCheck = (id: string) => {
setTasks((prevTasks) =>
prevTasks.map((task) => {
return task.id === id ? { ...task, completed: !task.completed } : task;
})
);
};
// Por último la lógica para eliminar, de nuevo es importante entender que no
// estamos mutando el valor de taks, con .filter() obtenemos todos los elementos
// diferentes por la operación que estamos realizando para discriminar
// actualizando cada uno de ellos y descartando la coincidencia de id.
const handleRemove = (id: string) => {
setTasks((prevTasks) => prevTasks.filter((task) => task.id !== id));
};Persistencia.
Ahora los hook de efecto.
useEffect(() => {
const storedTasks = localStorage.getItem("tasks");
if (storedTasks) {
setTasks(JSON.parse(storedTasks));
}
}, []);En el primer useEffect(), buscamos en localStorage un valor previo, si existiera, se parsea el string y se asigna a tasks.
Nota: A React no le gusta esto y tiene toda la razón, porque se puede hacer un efecto cascada y es que estamos cambiando el estado al momento de inicializar, aunque normalmente no se recomienda actualizar el estado en montaje, en este caso es seguro porque solo sincronizamos datos locales
Algo como lo de abajo sí termina en una ejecución en cascada.
useEffect(()=>{
setCount(count+1)
},[]) // con o sin array de dependencias está maluseEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);El segundo useEffect() tiene la importante misión de setear en localStorage el valor de tasks como un string en cada cambio de tasks, por eso lo agregamos en el array de dependencias. Así evitamos invocar esta lógica en cada handler que cambie tasks.
const taskCount = tasks.filter((t=>!t.completed)).length;
const sortedTasks = [...tasks].sort((a,b)=> a.priority - b.priority)Renderizado y prioridad
Estas últimas líneas antes del jsx surgieron para eliminar la lógica del componente, taskCount solo obtiene el número de tareas no completadas en el array tasks.
La variable sortedTasks contiene una copia de tasks ordenada de forma ascendente.
Habrán notado que usamos mucho este símbolo “…” ese operador se llama “Spread Operator” y es muuuuuy útil, grábenselo, hay que leer la documentación pero nos permite expandir todos los elementos de un array u objeto para manipularlos, aunque no sepamos cuales son.
Ejemplo:
setTasks((prevTasks) => [...prevTasks, { ...task, id }]);
// Acá [...prevTasks, { ...task, id }] nos dice, tasks ahora vale
// todos las tasks anteriores (...prevTasks) más un nuevo objeto {...task, id}
// que igual usa un spread para tomar todos los elementos de task y solo
// modificar la llave "id", que no se captura en formularioEn los handlers yo lo use para crear las copias de los elementos inmutables y asignarle un nuevo valor a tasks.
Por último el jsx del componente principal.
<main className="flex h-screen w-screen md:h-[70vh] md:w-[60vw] flex-col items-center justify-center p-5 bg-zinc-900 shadow shadow-rose-500/10 rounded-lg">
<form
className="bg-zinc-800 flex h-18 w-full md:w-1/2 text-black rounded-sm items-center justify-between p-3 gap-2"
onSubmit={handleSubmit}
>
<input
required
className="w-full h-10 bg-zinc-50 p-2 rounded-sm text-zinc-800"
type="text"
value={task.text}
onChange={handleChange}
name="text"
/>
<select
name="priority"
onChange={handleChange}
value={task.priority}
className="p-2 text-zinc-200"
>
{Object.entries(priorityMap).map(([key, value]) => (
<option key={key} value={Number(key)}>
{value.copy}
</option>
))}
</select>
<button
type="submit"
className="bg-rose-800 rounded-lg px-5 py-2 text-zinc-100 hover:opacity-70 cursor-pointer"
>
Add
</button>
</form>
<div className="grid grid-cols-1 md:grid-cols-2 w-full gap-2 p-2">
<div className="h-[50vh] overflow-scroll md:overflow-auto">
<h1 className="text-zinc-200 text-xl p-3 mx-auto w-full text-center my-2 uppercase">
Pending
</h1>
<ul className="flex flex-col gap-2 overflow-scroll md:overflow-auto px-5">
{sortedTasks
.filter((item) => !item.completed)
.map((it) => (
<TaskItem
key={it.id}
{...it}
onCheck={handleCheck}
onRemove={handleRemove}
/>
))}
</ul>
</div>
<div>
<h1 className="text-zinc-200 text-xl p-3 mx-auto w-full text-center my-2 uppercase">
Completed
</h1>
<ul className="flex flex-col gap-2 overflow-scroll md:overflow-auto px-5">
{sortedTasks
.filter((item) => item.completed)
.map((it) => (
<TaskItem
key={it.id}
{...it}
onCheck={handleCheck}
onRemove={handleRemove}
/>
))}
</ul>
</div>
</div>
<p className="text-zinc-200">
You have {taskCount} pending tasks
</p>
</main>Vamos a las partes importantes:
{Object.entries(priorityMap).map(([key, value]) => (
<option key={key} value={Number(key)}>
{value.copy}
</option>
))}Esta sintaxis es más moderna que hacer forEachs y nos ayuda cuando tenemos diccionario u otros objetos, nos permite iterar en ellos. Object.entries() nos va a retornar un par de valores, las llaves del objeto y los valores del objeto, Aquí usamos nuestro diccionario para generar los valores del option en el select de prioridad.
<ul className="flex flex-col gap-2 overflow-scroll md:overflow-auto px-5">
{sortedTasks
.filter((item) => !item.completed)
.map((it) => (
<TaskItem
key={it.id}
{...it}
onCheck={handleCheck}
onRemove={handleRemove}
/>
))}
</ul>Aquí usamos sortedTasks para filtrar los elementos donde item.completed === false.
Lo mismo hacemos para el caso de los completed === true y ya tenemos ambas listas preparadas para iterar TaskItem y mostrar los elementos de acuerdo a su prioridad y estado completed.
Para terminar regresamos a <TaskItem …>, habrán notado que tenemos en className del tag ‘li’ un objeto extraño (Si eres nuevo en JS)
<li
className={`flex h-10 bg-zinc-900 text-zinc-200 justify-between
items-center p-2 ${priorityMap[priority].styles} border rounded-lg`}
>¿Qué demonios es?
Nada del otro mundo,
Por un lado tenemos templates literals que solo es crear cadenas de texto insertando valores que serán evaluados como código en tiempo real, se usa con backticks → ` ` y se puede evaluar código de JS usando ${ -jscode-}, con jscode me refiero a lo que sea, variables, ternas, operaciones, métodos, ejemplos:
const mensaje = `Tu turno en KFC pollo es el ${getTurnFromDB()}`;
const dollarAmount= 10; // Monto en dolares
const totalInDollars = `${dollarAmount*18.6} MXN`; // Cantidad en pesosRegresando al tema, lo usamos porque va a depender del valor de ${priorityMap[priority].styles} y esto es solo notación.
Significa que vamos a buscar en priorityMap la coincidencia donde priority (que puede valer 0,1,2,3, ver TTask) en las llaves y accedemos al valor que es un objeto con mas llaves (text y styles), que son strings, entonces vamos a ejemplificar:
// Si tenemos que priority vale 2, la coincidencia es:
// 2: { copy: "Next week", styles: "border-green-500" },
// y accediendo a .styles nos regresara "border-green-500" para completar
// el className en tailwind.
// Queda entonces
<li
className="flex h-10 bg-zinc-900 text-zinc-200 justify-between
items-center p-2 border-green-500 border rounded-lg" >Terminamos! 🎉
Conclusión.
Con este ejercicio vimos cómo React maneja el estado, los efectos y el renderizado dinámico. Aunque el ejemplo es simple, ilustra conceptos clave: inmutabilidad, composición de componentes y persistencia local.
Si estás empezando con React, practicar con proyectos pequeños como este te da la base para escalar a apps más complejas sin perder claridad en la lógica.




