Peace & Code

Manejador de estados: Primer acercamiento

Publicado en

Conocimientos previos recomendados: React, Typescript

Este desarrollo nació de la necesidad de agrupar un conjunto de tareas por estado y que sea fácil moverlas entre los mismos. Quien no movió una tarea en un tablero de Trello o Jira no tuvo infancia 😆.

Primero, creemos el proyecto. Esto se puede hacer simplemente con React , pero por comodidad y costumbre lo armo en Next. Para crearlo usemos el comando que nos provee Next:

npx create-next-app@latest

Cuando creamos el proyecto, nos hace una serie de preguntas.

What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*

De estas configuraciones iniciales podemos sacar dos puntos interesantes para charlar.

  • Typescript VS Javascript: Tipar o no Tipar esa es la cuestión

  • Tailwind css: ¿Ahorrar tiempo o comprar problemas?

Mi opinión sobre Typescript es que es mejor tipar todo lo que se pueda y para lo que no, siempre tenemos a nuestro fiel amigo any para salvarnos las papas del fuego. Un código con muchos any's es lo mismo que no haber querido usar TS, por eso recomiendo eventualmente reemplazarlos por su correcta interfaz.

Con respecto a Tailwind css me pasa que como soy bastante nuevo en CSS, esta herramienta me brinda una buena escala de colores, padding, font, etc, para ordenarme. Se que mi pata más débil es el css y tengo pensado reforzarla con el curso CSS for JavaScript Developers (Aguante Josh 😅).


Sugerencia para el lector: Tomémonos un momento para pensar que modelos de datos podríamos crear para representar las distintas partes del problema (Las tareas y los distintos estados para cada tarea).


Los modelos de datos que se me ocurren para modelar son Tarea/Card y las Columnas donde las agrupamos por Estado:

export interface ICard {
id: string;
text: string;
status: Status;
initStatus: Status;
}

La interfaz ICard nos permite caracterizar, el texto, el estado y el id de la tarea. El estado inicial es un atributo opcional que decidí agregar para poder reiniciar una tarea. Podríamos intentar abstraer las Columnas/Estados con una interfaz, pero esta primera implementación dejemosla así y cuando terminemos revisemos qué más podríamos mover a una interfaz.

Si observan los tipo del atributo status y initStatus, no son un tipo primitivo de dato (string, number, etc). Para caracterizar el estado de una tarea cree un enum con los estados de mi tablero:

export enum Status {
Pending = "PENDINGS",
InProgress = "IN_PROGRESS",
Done = "DONE",
Frozen = "FROZEN",
}

Ahora creemos el componente StatusManager. Para crear un componente de manera rápida y ordenada uso la lib new-component, corriendo el siguiente comando, que configure en el package.json, para ejecutarlo con npm:

npm run new-component StatusManager

Bien ya tenemos el componente. Invoquemos a StatusManager en el page, pasandole una lista de tareas de prueba.


Sugerencia para el lector: Paremos a meditar qué es lo queremos construir. Hagámonos las siguientes preguntas . Si tuvieras que separar en partes el problema ¿cuáles son y cómo las llamarías? ¿Cuáles son las partes que se repiten? Tomate tu tiempo, una vez que tengas una respuesta seguí leyendo.


Ahora empieza lo interesante, es momento de armar los componentes de React. Los componentes que use son Card y Column tratando de representar la tarea y la columna/estado a la cuál movemos las tareas.

¿Qué responsablidad y qué datos debería tener el componente Card? El componente debería tener el texto de la tarea y una funcion para cambiarle el estado. Para cambiar el estado voy a usar tres botones:

  • Un flecha que indique a la derecha para avanzarlo de estado.

  • Una cruz para moverle a la columna de tareas congeladas ó paradas.

  • Un reload para reiniciarla.

Les dejo el codigo por si tienen dudas, les recomiendo fuertemente que antes de leerlo intenten hacer su propia implementeacion 😸 :

Componente Card
export default function Card({
card,
processCard,
cancel,
nextState,
}: {
card: ICard;
processCard: any;
cancel?: boolean;
nextState?: string;
}) {
const reload = card.status !== card.initStatus;
return (
<div className={classNames(styles.card, "pr-0")} key={card.id}>
<div className="flex flex-col justify-between h-full">
{cancel && (
<button className="none ">
<X
width={15}
height={12}
onClick={() => processCard(card.id, Status.Frozen)}
className="mr-1"
/>
</button>
)}
{reload && (
<button className="none ">
<RefreshCcw
width={15}
height={12}
onClick={() => processCard(card.id, card.initStatus)}
className="mr-1"
/>
</button>
)}
</div>
<div className="w-full">
<p>{card.text}</p>
</div>
{nextState && (
<button className="none">
<ChevronRight
width={20}
height={20}
onClick={() => processCard(card.id, nextState)}
className="m-auto ml-2 mr-0"
/>
</button>
)}
</div>
);
}

La función que se encarga de manejar ese cambio de estado la voy a delegar a su padre (StatusManager), recibiéndola como parámetro. Una pregunta valida seria ¿Porque tome esa decision? a lo que nos lleva a hablar de responsablidad de componentes.

Como vemos el punto de contacto entre Card y Column es StatusManager, él es quien tiene la responsabilidad de administrar las tareas, pasandole a cada Card sus datos ademas de una forma de avisarle cuando la misma cambie de estado y le da a Column la lista de tareas que debe mostrar.

En StatusManager vamos a agrupar las tareas por estado para poder mostrarlas en las columnas:

const pendingCards = cards.filter((card) => card.status === Status.Pending);
const inProgressCards = cards.filter(
(card) => card.status === Status.InProgress
);
const doneCards = cards.filter((card) => card.status === Status.Done);
const frozenCards = cards.filter((card) => card.status === Status.Frozen);

En el componente Column vamos a tener el título y la estética que queramos darle a cada columna para diferenciarlas. Luego de agrupar las tareas podemos invocar a Column pasando como children la lista de Cards:

<Column title="Pending" status={Status.Pending}>
{pendingCards?.map((card) => {
return (
<Card
key={card.id}
cancel={true}
card={card}
processCard={processCard}
nextState={Status.InProgress}
/>
);
})}
</Column>

Lo último que falta definir es la función que cambia de estado una tarea:

async function processCard(id: string, status: Status) {
const editCard: ICard | undefined = cards.find((c) => c.id === id);
if (!editCard) return;

if (status) {
const newCard: ICard = { ...editCard, id, status };
const newCards: ICard[] | undefined = cards.filter((c) => c.id !== id);
newCards?.unshift(newCard);
setCards(newCards);
}
}

Analicemos que hace esta función, primero busca la tarea por id y en caso de no encontrarlo termina la función con un return. Luego valida que le hayan pasado un estado. A partir de la vieja tarea crea una copia con el estado nuevo, después crea una copia de la lista de tareas quitando la vieja tarea, agrega la nueva al principio de la copia de la nueva de lista y por último llama a setCards pasandole la nueva lista.

Una observación que haría es "Nunca volviste a filtrar por estado, ¿Cómo es que las listas que agrupa por estado actualizaron sus tareas?". Esto pasa porque guarde las tareas dentro de useState.

export default function StatusManager({ initCards }: { initCards: ICard[] }) {
const [cards, setCards] = useState<ICard[]>(initCards);
...
}

Por cómo funciona React, cuando modificas un estado vuelve a actualizar sus dependencias y corre de nuevo la función StatusManager, haciendo también que se actualicen nuestras listas const.

Demo:

Frozen

Pending

task 1

task 2

task 3

In Progress

Done

Si tienen dudas o se trabaron al implementarpueden revisarlo en el git en el branch without-framer-motion .

Ahora que ya hicimos nuestra primera implementación podemos pensar si vale la pena agregar una interfaz para la Columna. En la misma podríamos ponerle el nombre de la columna, el color de fondo y otros aspectos de la columna que decidan agregar:

export interface IColumn {
id: string;
title: string;
status: Status;
}

Yo compuse el status con las partes del css, así el status define el fondo y el color del texto.

Los beneficios que trae abstraerlo en una interfaz es que podemos crear un array de IColumn e iterarlo ahorrandonos tener que escribir y filtrar por cada columna , asi reducimos la cantidad de codigo repetido, Además agregar una nueva columna es más sencillo, ya que ahora solo tenemos que agregar nuestra columna al array y definir su css.

Les recomiendo que intenten hacerlo como ejercicio para solidificar lo aprendido. No hay mejor manera para aprender que ensuciándose las manos con código 💪 .

Como se puede ver en la demo, las cards cambian instantáneamente de Columna a Columna, lo cual es feo visualmente. En el próximo Post vamos a meternos en el excitante mundo de animaciones con framer motion 😎.

Si quieren contactarme para darme feedback pueden escribirme a (mlpaz.code@gmail.com) .

Para enterarse cuando hay nuevo post siganme en Instagram.

Espero que les haya gustado y nos vemos en el próximo post.

🖖 Mr Peace and code

Nota: El diagrama de componentes fue generado con la libreria React Flow.