Category: Artículos

  • Angular 19 con Signals y Effects: Reactividad moderna sin RxJS

    Angular 19 con Signals y Effects: Reactividad moderna sin RxJS

    Con la llegada de Angular 16, y su consolidación en Angular 17, 18 y ahora 19, el equipo de Angular ha introducido una nueva forma de trabajar con la reactividad que se aleja de la complejidad de RxJS en muchos casos comunes: los Signals y Effects.

    En este post, exploraremos cómo crear un proyecto moderno con Angular 19 usando esta nueva API reactiva, y compararemos la solución con un enfoque clásico basado en BehaviorSubject. También presentaremos herramientas como Volta para controlar versiones de Node.js de forma consistente.

    Requisitos previos

    Versiones que usaremos:

    • Angular: 19.0.0 (estable en junio 2025)
    • Node.js: 20.x LTS
    • NPM: 10.x
    • Volta: última estable

    ¿Qué es Volta y por qué usarlo?

    Volta es un gestor de herramientas para entornos JavaScript que permite instalar y fijar versiones de herramientas como Node.js, npm, Yarn o pnpm. A diferencia de NVM, es más rápido, automático y confiable, ideal para entornos de desarrollo modernos y consistentes.

    Instalación de Volta

    En macOS con Homebrew:

    brew install volta

    O con el script oficial:

    curl https://get.volta.sh | bash

    Configuración del entorno

    Una vez instalado:

    volta install node@20
    volta install npm
    volta pin node@20

    Esto asegura que todos los comandos dentro del proyecto usen siempre Node.js 20.x, sin necesidad de .nvmrc o pasos manuales.

    Volta detecta automáticamente la configuración guardada en el package.json y la aplica en CLI, IDEs o entornos de CI/CD.

    Crear proyecto Angular

    Durante la creación del proyecto, Angular puede preguntarte si deseas habilitar SSR (Server-Side Rendering) o SSG (Static Site Generation). Para este tutorial, responde “No” a ambas, ya que estamos construyendo una SPA simple que consume una API pública desde el navegador.

    Instalación del Angular CLI

    Si aún no tienes Angular CLI instalado globalmente, puedes hacerlo con:

    npm install -g @angular/cli@19

    Esto te permitirá usar el comando ng para crear y administrar proyectos Angular.

    npm create @angular@latest
    # o si ya tienes Angular CLI
    ng new pokemon-signals-demo --standalone --routing --style=css
    cd pokemon-signals-demo

    Agregar Tailwind CSS al proyecto

    Desde Tailwind CSS v4, la forma recomendada de integración con Angular ha cambiado. Ya no se utiliza @tailwind en archivos SCSS ni se genera postcss.config.js, sino que se usa un archivo .postcssrc.json.

    Paso 1: Instalar Tailwind CSS

    npm install tailwindcss @tailwindcss/postcss postcss --force

    Paso 2: Crear el archivo .postcssrc.json

    En la raíz del proyecto, crea un archivo llamado .postcssrc.json con el siguiente contenido:

    {
      "plugins": {
        "@tailwindcss/postcss": {}
      }
    }

    Paso 3: Crear y configurar el archivo de estilos global

    Asegúrate de usar un archivo styles.css (no SCSS) dentro de src/, y en él escribe:

    Luego actualiza el archivo angular.json para asegurarte de que src/styles.css sea la hoja de estilo principal.

    Importante: Tailwind v4 no es compatible con preprocesadores CSS como SCSS. Usa solo .css para los estilos globales.

    Paso 4: Ejecutar la aplicación

    ng serve

    Qué son Signals y Effects

    signal es una forma de declarar un valor reactivo sin depender de RxJS. Funciona como una señal reactiva que notifica automáticamente a Angular para actualizar la vista o ejecutar efectos cuando cambia su valor.

    • signal: crea un valor observable reactivo.
    • computed: deriva valores de otros signals.
    • effect: ejecuta efectos secundarios (como peticiones HTTP o logs) en respuesta a cambios.

    Ejemplo real: Buscador de Pokémon con Angular Signals

    Vamos a crear un componente que consulte la API pública de PokeAPI y filtre los primeros 151 Pokémon por nombre.

    Crear el componente, servicio, tipos y configurar HttpClient

    Primero, generamos un componente standalone:

    ng generate component PokemonSearch --standalone

    También generamos un servicio:

    ng generate service pokemon --flat --path=src/app/pokemon

    Y creamos una carpeta para los tipos e interfaces del dominio:

    mkdir src/app/pokemon/types

    Dentro creamos el archivo de modelo:

    // src/app/pokemon/types/pokemon.model.ts
    export interface Pokemon {
      name: string;
      url: string;
    }

    Este servicio manejará la comunicación con la API. La interfaz Pokemon se separa en un archivo tipo para mantener una estructura limpia:

    import { Injectable, signal } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Pokemon } from '../types/pokemon.model';
    
    @Injectable({ providedIn: 'root' })
    export class PokemonService {
      private readonly apiUrl = 'https://pokeapi.co/api/v2/pokemon?limit=151';
    
      readonly pokemons = signal<Pokemon[]>([]);
    
      constructor(private http: HttpClient) {
        this.fetchAll();
      }
    
      fetchAll() {
        this.http.get<any>(this.apiUrl).subscribe(response => {
          this.pokemons.set(response.results);
        });
      }
    }
    

    Configurar main.ts para standalone con HttpClient

    Edita main.ts para usar PokemonSearchComponent como raíz:

    import { bootstrapApplication } from '@angular/platform-browser';
    import { provideHttpClient } from '@angular/common/http';
    import { PokemonSearchComponent } from './app/pokemon-search/pokemon-search.component';
    
    bootstrapApplication(PokemonSearchComponent, {
      providers: [
        provideHttpClient(),
      ]
    });

    Ver el resultado en el navegador

    Dado que PokemonSearchComponent es un componente standalone y está siendo bootstrapped directamente en main.ts, debes asegurarte de que su selector esté presente en index.html.

    Por defecto, el selector del componente es:

    selector: 'app-pokemon-search'

    Entonces, en tu archivo src/index.html, dentro del <body>, agrega:

    <body>
      <app-pokemon-search></app-pokemon-search>
    </body>

    Esto le dice a Angular dónde montar el componente en el DOM. Si no colocas ese selector, obtendrás un error como:

    NG05104: The selector "app-pokemon-search" did not match any elements

    También podrías cambiar el selector a 'app-root' si prefieres mantener la convención típica de Angular y usar <app-root> en el HTML.

    Dado que PokemonSearchComponent es un componente standalone y está siendo bootstrapped directamente en main.ts, el contenido se mostrará en el index.html por defecto de Angular, en el elemento <app-root> (o en su reemplazo si se cambia el selector).

    Asegúrate de que el selector usado sea <app-pokemon-search> o lo que corresponda, y que esté vinculado directamente desde main.ts como se muestra arriba.

    Vamos a construir una aplicación que cargue la lista de los primeros 151 Pokémon desde la API pública PokeAPI y permita filtrarlos por nombre con un input reactivo.

    import { Component, computed, signal, effect } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { PokemonService } from '../pokemon/pokemon.service';
    
    @Component({
      selector: 'app-pokemon-search',
      standalone: true,
      imports: [CommonModule],
      templateUrl: './pokemon-search.component.html',
    })
    export class PokemonSearchComponent {
      // Signal que almacena el valor actual del input
      searchTerm = signal('');
    
      // Valor computado que depende del signal searchTerm y del signal del servicio
      filteredPokemons = computed(() => {
        const term = this.searchTerm().toLowerCase();
        return this.pokemonService.pokemons().filter(p =>
          p.name.toLowerCase().includes(term)
        );
      });
    
      // Inyectamos el servicio donde vive el signal con la data
      constructor(public pokemonService: PokemonService) {
        // Opcional: efecto secundario al cambiar el valor del input
        effect(() => {
          console.log('Término de búsqueda:', this.searchTerm());
        });
      }
    
      // Función que actualiza el signal searchTerm al escribir en el input
      updateSearch(term: string) {
        this.searchTerm.set(term);
      }
    }
    

    ¿Qué son signal, computed y effect?

    • signal(initialValue): Crea un valor reactivo que notifica automáticamente cuando cambia. Similar a un BehaviorSubject, pero más simple y sin necesidad de subscribe.

    • computed(() => ...): Deriva un valor a partir de uno o varios signals. Solo se recalcula cuando cambian sus dependencias. Ideal para filtros, cálculos derivados, etc.

    • effect(() => ...): Ejecuta un bloque de código cada vez que cambian los signals usados dentro del efecto. Útil para side-effects como logging, tracking, llamadas a APIs, etc.

    En este tutorial solo usamos effect como demostración, pero podrías aprovecharlo para:

    • Guardar búsquedas recientes.

    • Sincronizar con el localStorage.

    • Disparar una petición HTTP cada vez que cambie el filtro (aunque no es necesario en este caso porque ya precargamos los 151 Pokémon).

    Template HTML

    <div class="flex flex-col items-center justify-center h-screen w-screen">
      <div class="w-4/5 min-h-[500px] bg-gray-100 rounded-lg p-4 overflow-y-auto my-8">
        <input
          type="text"
          class="w-full p-2 rounded-md border border-gray-300 bg-white"
          placeholder="Buscar Pokémon..."
          (input)="updateSearch($any($event).target.value)"
        />
        <div class="grid grid-cols-6 gap-4 mt-4">
          <div *ngFor="let pokemon of filteredPokemons()" 
               class="bg-white p-4 rounded-lg border border-gray-200">
            <p class="text-center capitalize">{{ pokemon.name }}</p>
          </div>
        </div>
      </div>
    </div>
    

    Comparativa: Cómo se hacía antes con RxJS

    A continuación, vemos cómo implementar el mismo comportamiento usando RxJS y el enfoque clásico basado en observables:

    // Importamos decoradores y operadores necesarios
    import { Component, OnInit } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { BehaviorSubject, combineLatest, map } from 'rxjs';
    
    @Component({
      selector: 'app-pokemon-search-rxjs',
      standalone: true,
      templateUrl: './pokemon-search.component.html',
    })
    export class PokemonSearchRxjsComponent implements OnInit {
      // URL de la API
      private apiUrl = 'https://pokeapi.co/api/v2/pokemon?limit=151';
    
      // Observable que representa el término de búsqueda
      private searchTerm$ = new BehaviorSubject<string>('');
    
      // Observable que obtiene todos los Pokémon desde la API
      private allPokemons$ = this.http.get<any>(this.apiUrl).pipe(
        map(res => res.results)
      );
    
      // Combina ambos observables y filtra los resultados según el término
      filteredPokemons$ = combineLatest([this.allPokemons$, this.searchTerm$]).pipe(
        map(([pokemons, term]) =>
          pokemons.filter((p: any) =>
            p.name.toLowerCase().includes(term.toLowerCase())
          )
        )
      );
    
      constructor(private http: HttpClient) {}
    
      // Método de Angular, aunque no es necesario en este ejemplo
      ngOnInit(): void {}
    
      // Función para actualizar el término de búsqueda
      updateSearch(term: string) {
        this.searchTerm$.next(term);
      }
    }

    Comparación línea por línea

    Angular Signals RxJS clásico
    signal('') new BehaviorSubject('')
    computed(() => ...) combineLatest([...]).pipe(map(...))
    effect(() => ...) subscribe(...) (si hiciera falta)
    .set() .next()
    Reactividad declarativa integrada Reactividad con suscripción explícita
    Sin necesidad de unsubscribe Debes cuidar las suscripciones manuales

    Ventajas de usar Signals

    • No necesitas suscripciones ni unsubscribe.
    • Menos boilerplate.
    • Integración directa con Angular y detección de cambios optimizada.
    • Código más declarativo y legible.

    Conclusión

    Angular 19 marca una evolución en la forma de trabajar con reactividad. Signals y Effects ofrecen una experiencia más directa, clara y moderna, ideal para la mayoría de los casos donde antes usábamos RxJS por obligación. Aunque RxJS sigue siendo útil en escenarios complejos, Signals resuelven el 80% de los casos comunes con menos código y más claridad.

    En futuros posts podríamos combinar Signals con rutas, inputs y servicios para crear apps más complejas, o explorar cómo coexistir con código existente basado en RxJS.

  • Arquitectura moderna Microfrontends: Caso real TCG Pocket – II

    Arquitectura moderna Microfrontends: Caso real TCG Pocket – II

    Ahora comenzamos la parte práctica del proyecto:

    ✅ Creamos el monorepo con Nx.
    ✅ Generamos la app shell, que servirá de host para los microfrontends.
    ✅ Configuramos next-i18next para traducción en español e inglés.
    ✅ Verificamos que el servidor funcione localmente y muestra el texto traducido correctamente.

    Paso 1: Crear el monorepo con Nx

    Usaremos Nx por su robustez y soporte para múltiples frameworks (Next.js, NestJS, React, etc.).

    npx create-nx-workspace@latest tcg-pocket --preset=apps --package-manager=npm --workspaceType=integrated

    Explicación:

    • tcg-pocket: nombre del workspace.

    • --preset=apps: crearemos un monorepo sin proyectos iniciales.

    • --package-manager=npm: usaremos npm (puedes usar pnpm o yarn si prefieres).

     

    Paso 2: Instalar plugins para Next.js y NestJS

    npm install --save-dev @nx/next @nx/nest
    • @nx/next: para apps Next.js.

    • @nx/nest: para el backend BFF con NestJS.

    Paso 3: Generar la aplicación shell (Next.js)

    npx nx g @nx/next:app apps/shell --style=tailwind

    Esto crea:

    /apps/shell

    Con todos los archivos de un proyecto Next.js básico.

    Paso 4: Añadir soporte para next-i18next

    Instalamos la librería:

    npm install next-i18next
    

    Creamos la configuración base de i18n en la raíz del shell:

    // next-i18next.config.js
    module.exports = {
      i18n: {
        defaultLocale: 'es',
        locales: ['es', 'en'],
      },
    };
    

    Paso 5: Configurar Next.js para usar i18n

    Edita /apps/shell/next.config.js:

    //@ts-check
    
     
    const { composePlugins, withNx } = require('@nx/next');
    const { i18n } = require('./next-i18next.config');
    
    /**
     * @type {import('@nx/next/plugins/with-nx').WithNxOptions}
     **/
    const nextConfig = {
      reactStrictMode: true,
      i18n,
      nx: {},
    };
    
    const plugins = [
      withNx,
    ];
    
    module.exports = composePlugins(...plugins)(nextConfig);
    
    

    Paso 6: Crear archivos de traducción

    Creamos carpetas de traducción dentro de la app shell:

    /apps/shell/public/locales
      /es
        common.json
      /en
        common.json
    

    Ejemplo de /apps/shell/public/locales/es/common.json:

    {
      "title": "Bienvenido a TCG Pocket Explorer",
      "description": "Explora cartas de Pokémon TCG Pocket con arquitectura moderna"
    }

    Y para /en/common.json:

    {
      "title": "Welcome to TCG Pocket Explorer",
      "description": "Explore Pokémon TCG Pocket cards with modern architecture"
    }
    

    Paso 7: Usar traducción en la app

    Edita /apps/shell/pages/index.tsx:

    import { useTranslation } from 'next-i18next';
    import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
    
    export default function Home() {
      const { t } = useTranslation('common');
    
      return (
        <main className="flex flex-col items-center justify-center min-h-screen">
          <h1 className="text-3xl font-bold">{t('title')}</h1>
          <p className="mt-2 text-gray-600">{t('description')}</p>
        </main>
      );
    }
    
    // Para que Next.js cargue las traducciones en SSR:
    export async function getStaticProps({ locale }: { locale: string }) {
      return {
        props: {
          ...(await serverSideTranslations(locale, ['common'])),
        },
      };
    }
    

    Editar el archivo pages/_app.tsx:

    import { AppProps } from 'next/app';
    import Head from 'next/head';
    import './styles.css';
    import { appWithTranslation } from 'next-i18next';
    
    function CustomApp({ Component, pageProps }: AppProps) {
      return (
        <>
          <Head>
            <title>Welcome to shell!</title>
          </Head>
          <main className="app">
            <Component {...pageProps} />
          </main>
        </>
      );
    }
    
    export default appWithTranslation(CustomApp);

    Esto nos permitirá usar la traducción en toda la web.

    Paso 8: Levantar el servidor y probar

    Arrancamos la app shell:

    nx run shell:dev

    Esto corre en http://localhost:3000.

    Probar el cambio de idioma

    Para ver cómo funciona la traducción:

    • Accede a http://localhost:3000/es → ves la versión en español.

    • Accede a http://localhost:3000/en → ves la versión en inglés.

    ¡La traducción ya está activa y funcionando!

    ¿Qué logramos?

    ✅ Monorepo base creado y estructurado con Nx.
    ✅ App shell Next.js lista como host de microfrontends.
    ✅ Configuración de i18n con next-i18next y traducciones en español e inglés.
    ✅ Servidor corriendo y funcionando correctamente.

    ¿Qué sigue?

    En el próximo post:

    • Agregaremos los microfrontends sets y cards.

    • Configuraremos Module Federation para que el shell cargue sus módulos remotos.

    • Documentaremos la configuración y primeras integraciones.

  • Cómo construir un Multiselect Dropdown en 4 Frameworks modernos

    Cómo construir un Multiselect Dropdown en 4 Frameworks modernos

    Vamos a construir un componente multiselect desde cero usando React y Tailwind CSS, que nos permitirá seleccionar múltiples opciones desde un dropdown y mostrar en consola las opciones seleccionadas. Empezaremos con React ya que es lo que más hemos visto hasta ahora.

    Paso 1: Crear la estructura base del componente

    Primero vamos a crear el layout base del componente usando sólo HTML y clases de Tailwind. Esto nos permite enfocarnos primero en el diseño sin preocuparnos por la lógica todavía.

    Creamos un archivo: MultiSelectDropdown.tsx Y pegamos esta estructura:

    export default function MultiSelectDropdown() {
      return (
        <div className="w-60 m-auto">
          <button
            type="button"
            className="text-white justify-between w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
          >
            Dropdown button
            <svg
              className="w-2.5 h-2.5 ml-3"
              viewBox="0 0 10 6"
              fill="none"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M1 1L5 5L9 1"
                stroke="currentColor"
                strokeWidth="2"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
          </button>
    
          {/* Dropdown visible por ahora */}
          <div className="z-10 w-full mt-2 block bg-white divide-y divide-gray-100 rounded-lg shadow-sm dark:bg-gray-700">
            <ul className="p-3 space-y-3 text-sm text-gray-700 dark:text-gray-200">
              <li>
                <label className="flex items-center gap-2">
                  <input type="checkbox" />
                  React
                </label>
              </li>
              <li>
                <label className="flex items-center gap-2">
                  <input type="checkbox" />
                  Vue
                </label>
              </li>
            </ul>
          </div>
        </div>
      );
    }
    

    ¿Qué hicimos aquí?

    • Creamos un contenedor de ancho fijo centrado.

    • Agregamos un botón con estilos de Tailwind (bg-blue-700, rounded-lg, etc.).

    • Insertamos una lista de opciones de ejemplo.

    • Todo el diseño es estático todavía, sin funcionalidad.

    Paso 2: Manejar la apertura y cierre del dropdown

    Ahora vamos a hacer que el dropdown se oculte o muestre cuando hacemos clic en el botón.

    import { useState } from "react";
    
    export default function MultiSelectDropdown() {
      const [isOpen, setIsOpen] = useState(false);
    
      return (
        <div className="w-60 m-auto">
          <button
            type="button"
            onClick={() => setIsOpen((prev) => !prev)}
            className="text-white justify-between w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
          >
            Dropdown button
            <svg
              className="w-2.5 h-2.5 ml-3"
              viewBox="0 0 10 6"
              fill="none"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M1 1L5 5L9 1"
                stroke="currentColor"
                strokeWidth="2"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
          </button>
    
          {isOpen && (
            <div className="z-10 w-full mt-2 block bg-white divide-y divide-gray-100 rounded-lg shadow-sm dark:bg-gray-700">
              <ul className="p-3 space-y-3 text-sm text-gray-700 dark:text-gray-200">
                <li>
                  <label className="flex items-center gap-2">
                    <input type="checkbox" />
                    React
                  </label>
                </li>
                <li>
                  <label className="flex items-center gap-2">
                    <input type="checkbox" />
                    Vue
                  </label>
                </li>
              </ul>
            </div>
          )}
        </div>
      );
    }
    

    ¿Qué aprendemos aquí?

    • React usa useState para manejar el estado de apertura del menú.

    • Con {isOpen && (...)} mostramos u ocultamos el dropdown de forma condicional.

    • Esto ya nos permite ver el menú al hacer clic, y ocultarlo al hacer clic de nuevo.

    Paso 3: Crear opciones dinámicas

    Ahora definimos una lista de opciones para renderizarlas dinámicamente. Así podemos cambiar fácilmente las opciones más adelante sin repetir código (Por el momento dentro del componente pero se puede fácilmente pasar como prop).

    const options = [
      { label: "React", value: "react" },
      { label: "Vue", value: "vue" },
      { label: "Svelte", value: "svelte" },
      { label: "Solid", value: "solid" },
      { label: "Angular", value: "angular" },
    ];
    

    Y en lugar de escribir <li> manualmente, iteramos con .map():

    <ul className="p-3 space-y-3 text-sm text-gray-700 dark:text-gray-200">
      {options.map((option) => (
        <li key={option.value}>
          <label className="flex items-center gap-2">
            <input type="checkbox" />
            {option.label}
          </label>
        </li>
      ))}
    </ul>
    

    Paso 4: Guardar y manejar las opciones seleccionadas

    Ahora implementamos la lógica para guardar las selecciones. Creamos una nueva variable de estado llamada selected que guardará un array con los valores seleccionados.

    Además, queremos que cada vez que se seleccione o deseleccione algo, se haga un console.log() del array actualizado. Para evitar logs duplicados, lo haremos con useEffect.

    import { useState, useEffect } from "react";
    
    export default function MultiSelectDropdown() {
      const [isOpen, setIsOpen] = useState(false);
      const [selected, setSelected] = useState<string[]>([]);
    
      // Este efecto imprime en consola cada vez que cambia la selección
      useEffect(() => {
        console.log("Seleccionadas:", selected);
      }, [selected]);
    
      const handleChange = (value: string) => {
        setSelected((prev) =>
          prev.includes(value)
            ? prev.filter((v) => v !== value)
            : [...prev, value]
        );
      };
    
      const options = [
        { label: "React", value: "react" },
        { label: "Vue", value: "vue" },
        { label: "Svelte", value: "svelte" },
        { label: "Solid", value: "solid" },
        { label: "Angular", value: "angular" },
      ];
    
      return (
        <div className="w-60 m-auto">
          <button
            onClick={() => setIsOpen((prev) => !prev)}
            type="button"
            className="text-white justify-between w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center"
          >
            Dropdown button
            <svg className="w-2.5 h-2.5 ml-3" /* ... */ />
          </button>
    
          {isOpen && (
            <div className="z-10 w-full mt-2 block bg-white rounded-lg shadow-sm">
              <ul className="p-3 space-y-3 text-sm text-gray-700">
                {options.map((option) => (
                  <li key={option.value}>
                    <label className="flex items-center gap-2">
                      <input
                        type="checkbox"
                        checked={selected.includes(option.value)}
                        onChange={() => handleChange(option.value)}
                      />
                      {option.label}
                    </label>
                  </li>
                ))}
              </ul>
            </div>
          )}
        </div>
      );
    }
    

    Resultado

    Ya tenemos un componente 100% funcional en React:

    • Visualmente estilizado con Tailwind.

    • Dropdown desplegable al clic.

    • Permite seleccionar y deseleccionar múltiples opciones.

    • Imprime en consola las opciones seleccionadas sin duplicados.

     

    Multiselect Dropdown en Vue 3 + Tailwind + TypeScript

    Aquí replicamos el mismo componente de React, pero usando Vue 3, Composition API, TypeScript y Tailwind. Lo haremos desde cero, en pasos simples y comentados.

    • Mostrará un botón para desplegar un menú con opciones.

    • Permitirá seleccionar múltiples opciones (checkboxes).

    • Al seleccionar/deseleccionar, actualizará un array reactivo.

    • Imprimirá en consola las opciones seleccionadas (sin duplicados).

    • Usará Tailwind CSS para estilos.

     

    Paso 1: Crear la estructura del componente

    Creamos el archivo:

    src/components/MultiSelectDropdown.vue
    

    Y comenzamos con la estructura mínima:

    <template>
      <div class="w-60 m-auto">
        <button
          type="button"
          class="text-white justify-between w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center"
        >
          Dropdown button
          <svg
            class="w-2.5 h-2.5 ml-3"
            viewBox="0 0 10 6"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              d="M1 1L5 5L9 1"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </button>
    
        <div class="z-10 w-full mt-2 block bg-white divide-y divide-gray-100 rounded-lg shadow-sm">
          <ul class="p-3 space-y-3 text-sm text-gray-700">
            <li>
              <label class="flex items-center gap-2">
                <input type="checkbox" />
                React
              </label>
            </li>
            <li>
              <label class="flex items-center gap-2">
                <input type="checkbox" />
                Vue
              </label>
            </li>
          </ul>
        </div>
      </div>
    </template>
    
    <script setup lang="ts">
    // Lógica vendrá después
    </script>
    

    ¿Qué hicimos aquí?

    • Creamos la estructura visual: un botón y un dropdown con dos opciones fijas.

    • Usamos Tailwind para el diseño (botón azul, checkbox alineado).

    • No hay interactividad aún: ni apertura/cierre, ni selección.

    Este paso sirve para tener el esqueleto visual listo antes de agregar la lógica.

    Paso 2: Mostrar/Ocultar el dropdown con ref()

    Queremos que el dropdown solo se muestre cuando hagamos clic en el botón. Para eso:

    • Creamos una variable reactiva llamada isOpen

    • Usamos v-if="isOpen" para mostrar el menú

    • Alternamos el valor con un método toggleDropdown()

    Actualizamos el componente así:

    <template>
      <div class="w-60 m-auto">
        <!-- Botón que abre/cierra el dropdown -->
        <button
          type="button"
          @click="toggleDropdown"
          class="text-white justify-between w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center"
        >
          Dropdown button
          <svg
            class="w-2.5 h-2.5 ml-3"
            viewBox="0 0 10 6"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              d="M1 1L5 5L9 1"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </button>
    
        <!-- Mostramos el dropdown solo si isOpen es true -->
        <div
          v-if="isOpen"
          class="z-10 w-full mt-2 block bg-white divide-y divide-gray-100 rounded-lg shadow-sm"
        >
          <ul class="p-3 space-y-3 text-sm text-gray-700">
            <li>
              <label class="flex items-center gap-2">
                <input type="checkbox" />
                React
              </label>
            </li>
            <li>
              <label class="flex items-center gap-2">
                <input type="checkbox" />
                Vue
              </label>
            </li>
          </ul>
        </div>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    
    const isOpen = ref(false)
    
    const toggleDropdown = () => {
      isOpen.value = !isOpen.value
    }
    </script>
    

    ¿Qué aprendimos aquí?

    • ref(false) crea una variable reactiva en Vue.

    • Usamos v-if="isOpen" para mostrar el menú condicionalmente.

    • Al hacer clic en el botón, toggleDropdown() invierte su valor (true / false).

    Ya tenemos la interacción básica lista. ¡Se ve como un dropdown real!

    Paso 3: Mostrar opciones desde un array con v-for

    Hasta ahora, las opciones están “quemadas” en el HTML. Vamos a moverlas a un array para que el componente sea dinámico y reusable.

    1. Creamos un array de opciones en el <script setup>

    Usamos TypeScript para tipar las opciones:

    interface Option {
      label: string
      value: string
    }
    
    const options: Option[] = [
      { label: 'React', value: 'react' },
      { label: 'Vue', value: 'vue' },
      { label: 'Svelte', value: 'svelte' },
      { label: 'Solid', value: 'solid' },
      { label: 'Angular', value: 'angular' }
    ]
    

    2. Reemplazamos los <li> fijos por un v-for

    Actualizamos el template:

    <ul class="p-3 space-y-3 text-sm text-gray-700">
      <li v-for="option in options" :key="option.value">
        <label class="flex items-center gap-2">
          <input type="checkbox" />
          {{ option.label }}
        </label>
      </li>
    </ul>
    

    ¿Qué logramos aquí?

    • El componente ahora usa un array tipado con Option[], lo que facilita reusarlo.

    • Con v-for, renderizamos tantas opciones como queramos sin duplicar código.

    • Vue se encarga de mantener la eficiencia del DOM con :key="option.value".

    Paso 4: Manejar selección múltiple (checkboxes)

    Ahora queremos que:

    • Cada checkbox represente una opción seleccionable.

    • Las opciones seleccionadas se guarden en un array selected.

    • Si ya está seleccionada, se deseleccione (toggle).

    • Se imprima en consola cada vez que el array cambie.

    1. Creamos el estado selected

    En el <script setup>:

    import { ref, watch } from 'vue'
    
    const selected = ref<string[]>([])
    

    2. Creamos una función para agregar o quitar selecciones

    const toggleSelection = (value: string) => {
      if (selected.value.includes(value)) {
        selected.value = selected.value.filter(v => v !== value)
      } else {
        selected.value.push(value)
      }
    }
    

    3. Escuchamos los cambios con watch (como useEffect en React)

    Usamos :checked y @change para enlazar con el estado:

    <input
      type="checkbox"
      :checked="selected.includes(option.value)"
      @change="toggleSelection(option.value)"
    />
    

    Resultado

    Ahora el componente:

    • Permite seleccionar múltiples opciones.

    • Guarda las seleccionadas en un array.

    • Hace console.log() al cambiar la selección.

     

    Multiselect Dropdown en Svelte + Tailwind + TypeScript

    Paso 1: Estructura HTML + Tailwind base

    Vamos a comenzar por construir solo la estructura visual estática del componente, sin lógica aún. El objetivo es ver cómo se verá el dropdown, el botón y las opciones con checkboxes, usando únicamente HTML y Tailwind CSS.

    Código inicial: MultiSelectDropdown.svelte

    <script lang="ts">
      // lógica vendrá después
    </script>
    
    <div class="w-60 m-auto">
      <!-- Botón para abrir el dropdown -->
      <button
        type="button"
        class="text-white justify-between w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center"
      >
        Dropdown button
        <svg
          class="w-2.5 h-2.5 ml-3"
          viewBox="0 0 10 6"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            d="M1 1L5 5L9 1"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </button>
    
      <!-- Dropdown (visible todo el tiempo por ahora) -->
      <div
        class="z-10 w-full mt-2 block bg-white divide-y divide-gray-100 rounded-lg shadow-sm"
      >
        <ul class="p-3 space-y-3 text-sm text-gray-700">
          <li>
            <label class="flex items-center gap-2">
              <input type="checkbox" />
              React
            </label>
          </li>
          <li>
            <label class="flex items-center gap-2">
              <input type="checkbox" />
              Vue
            </label>
          </li>
        </ul>
      </div>
    </div>
    

    ¿Qué tenemos hasta ahora?

    • Un botón con diseño responsivo (w-full, bg-blue-700, hover, rounded-lg).

    • Un ícono de flecha hacia abajo (svg).

    • Un menú tipo dropdown con dos opciones de ejemplo (React, Vue) como <li>.

    • Cada opción es un <label> que contiene un checkbox y un texto.

    Paso 2: Mostrar/Ocultar el dropdown con una variable reactiva

    Ahora vamos a hacer que el dropdown se abra y cierre al hacer clic en el botón, usando reactividad en Svelte con let.

    ¿Cómo lo haremos?

    • Creamos una variable isOpen con let

    • Alternamos su valor al hacer clic en el botón

    • Mostramos el dropdown solo si isOpen === true usando {#if}

    Código actualizado

    <script lang="ts">
      let isOpen = false;
    
      function toggleDropdown() {
        isOpen = !isOpen;
      }
    </script>
    
    <div class="w-60 m-auto">
      <!-- Botón con evento on:click -->
      <button
        type="button"
        on:click={toggleDropdown}
        class="text-white justify-between w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center"
      >
        Dropdown button
        <svg
          class="w-2.5 h-2.5 ml-3"
          viewBox="0 0 10 6"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            d="M1 1L5 5L9 1"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </button>
    
      <!-- Mostrar dropdown solo si isOpen es true -->
      {#if isOpen}
        <div
          class="z-10 w-full mt-2 block bg-white divide-y divide-gray-100 rounded-lg shadow-sm"
        >
          <ul class="p-3 space-y-3 text-sm text-gray-700">
            <li>
              <label class="flex items-center gap-2">
                <input type="checkbox" />
                React
              </label>
            </li>
            <li>
              <label class="flex items-center gap-2">
                <input type="checkbox" />
                Vue
              </label>
            </li>
          </ul>
        </div>
      {/if}
    </div>
    

    ¿Qué aprendimos aquí?

    • En Svelte, let crea variables reactivas automáticamente.

    • on:click={toggleDropdown} es la forma de escuchar eventos.

    • {#if ...} es como v-if en Vue o un render condicional en React ({isOpen && (...)}).

     

    Paso 3: Mostrar las opciones dinámicamente con {#each}

    Hasta ahora, las opciones (React, Vue) están escritas directamente en el HTML. Ahora vamos a moverlas a un array tipado para que el componente sea dinámico y escalable.

    1. Creamos el array de opciones en el <script>

    interface Option {
      label: string;
      value: string;
    }
    
    const options: Option[] = [
      { label: 'React', value: 'react' },
      { label: 'Vue', value: 'vue' },
      { label: 'Svelte', value: 'svelte' },
      { label: 'Solid', value: 'solid' },
      { label: 'Angular', value: 'angular' },
    ];
    

    Option es una interfaz que usamos para tipar las opciones. Esto es posible gracias a que activaste TypeScript

    2. Reemplazamos los <li> fijos por {#each}

    <ul class="p-3 space-y-3 text-sm text-gray-700">
      {#each options as option (option.value)}
        <li>
          <label class="flex items-center gap-2">
            <input type="checkbox" />
            {option.label}
          </label>
        </li>
      {/each}
    </ul>
    

    ¿Qué logramos?

    • Las opciones ya no están duplicadas manualmente.

    • Podemos fácilmente agregar, quitar o pasar las opciones como props después.

    • Cada opción se representa con un checkbox y su label.

    Paso 4: Manejar selección múltiple y mostrarla en consola

    Queremos que el componente:

    1. Permita seleccionar múltiples opciones (con checkboxes).

    2. Guarde las opciones seleccionadas en un array.

    3. Haga console.log() del array cada vez que cambie.

    ¿Cómo lo haremos?

    • Creamos un array reactivo selected: string[].

    • Usamos bind:checked para vincular cada checkbox con el estado.

    • O mejor: manejamos el on:change manualmente, como hicimos en Vue y React (para control total).

    • Imprimimos con $: console.log(...) que es como watch o useEffect.

    1. Creamos selected y toggleSelection()

    let selected: string[] = [];
    
    function toggleSelection(value: string) {
      if (selected.includes(value)) {
        selected = selected.filter((v) => v !== value);
      } else {
        selected = [...selected, value];
      }
    }
    
    // log reactivo
    $: console.log('Seleccionadas:', selected);
    

    2. Actualizamos los checkboxes

    En cada input, usamos:

    <input
      type="checkbox"
      checked={selected.includes(option.value)}
      on:change={() => toggleSelection(option.value)}
    />
    

    ¿Qué logramos?

    • El dropdown ya permite selección múltiple.

    • Los checkboxes reflejan el estado real.

    • Cada cambio en selected hace un console.log() sin duplicados.

    • Comportamiento igual al de React y Vue

    Multiselect Dropdown en Angular + Tailwind + TypeScript

    Paso 1: Estructura HTML + Tailwind base

    Primero, creamos solo la estructura visual del componente, sin lógica aún. Esto nos permite ver el diseño del botón y el menú desplegable.

    1. Crear el componente

    Desde la terminal, en el proyecto Angular:

    ng generate component components/multi-select-dropdown
    

    Esto creará 4 archivos dentro de src/app/components/multi-select-dropdown:

    • multi-select-dropdown.component.ts

    • multi-select-dropdown.component.html

    • multi-select-dropdown.component.css

    • multi-select-dropdown.component.spec.ts

     

    2. Usar el componente en app.component.html

    Edita el archivo src/app/app.component.html y agrega:

    <app-multi-select-dropdown></app-multi-select-dropdown>
    

    3. Estructura HTML base en multi-select-dropdown.component.html

    Pegamos la estructura visual básica con Tailwind:

    <div class="w-60 m-auto">
      <!-- Botón que abre el dropdown -->
      <button
        type="button"
        class="text-white justify-between w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center"
      >
        Dropdown button
        <svg
          class="w-2.5 h-2.5 ml-3"
          viewBox="0 0 10 6"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            d="M1 1L5 5L9 1"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </button>
    
      <!-- Dropdown visible por ahora (sin lógica) -->
      <div
        class="z-10 w-full mt-2 block bg-white divide-y divide-gray-100 rounded-lg shadow-sm"
      >
        <ul class="p-3 space-y-3 text-sm text-gray-700">
          <li>
            <label class="flex items-center gap-2">
              <input type="checkbox" />
              React
            </label>
          </li>
          <li>
            <label class="flex items-center gap-2">
              <input type="checkbox" />
              Vue
            </label>
          </li>
        </ul>
      </div>
    </div>
    

    Resultado esperado

    Si recargas http://localhost:4200, deberías ver:

    • Un botón azul que dice “Dropdown button”

    • Un dropdown con dos opciones (React y Vue)

    • Estilo limpio con Tailwind

    Paso 2: Mostrar/Ocultar el dropdown con una variable en el componente

    Queremos que el dropdown:

    • Se muestre al hacer clic en el botón.

    • Se oculte al volver a hacer clic.

    ¿Cómo lo haremos?

    1. Creamos una propiedad booleana isOpen en el .ts.

    2. Hacemos toggle con un método toggleDropdown().

    3. En el HTML, usamos *ngIf="isOpen" para mostrar/ocultar el menú.

    1. Agregar lógica en el archivo .ts

    Edita multi-select-dropdown.component.ts:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-multi-select-dropdown',
      templateUrl: './multi-select-dropdown.component.html',
      styleUrls: ['./multi-select-dropdown.component.css'],
    })
    export class MultiSelectDropdownComponent {
      isOpen = false;
    
      toggleDropdown(): void {
        this.isOpen = !this.isOpen;
      }
    }
    

    2. Conectar el botón al método

    Edita multi-select-dropdown.component.html:

    <!-- Botón con (click) -->
    <button
      type="button"
      (click)="toggleDropdown()"
      class="text-white justify-between w-full bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 inline-flex items-center"
    >
      Dropdown button
      <svg
        class="w-2.5 h-2.5 ml-3"
        viewBox="0 0 10 6"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M1 1L5 5L9 1"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        />
      </svg>
    </button>
    

    3. Mostrar el dropdown condicionalmente

    Reemplaza el div del dropdown así:

    <div
      *ngIf="isOpen"
      class="z-10 w-full mt-2 block bg-white divide-y divide-gray-100 rounded-lg shadow-sm"
    >
      <ul class="p-3 space-y-3 text-sm text-gray-700">
        <li>
          <label class="flex items-center gap-2">
            <input type="checkbox" />
            React
          </label>
        </li>
        <li>
          <label class="flex items-center gap-2">
            <input type="checkbox" />
            Vue
          </label>
        </li>
      </ul>
    </div>
    

    Resultado

    Ahora al hacer clic en el botón:

    • El menú aparece y desaparece.

    • El estado lo maneja isOpen con un simple toggle.

    Paso 3: Mostrar las opciones dinámicamente desde un array

    Hasta ahora, las opciones (React, Vue) están escritas directamente en el HTML. Vamos a moverlas a un array tipado en TypeScript y usar *ngFor para renderizarlas dinámicamente.

    1. Declarar la interfaz y el array de opciones

    Edita multi-select-dropdown.component.ts:

    interface Option {
      label: string;
      value: string;
    }
    
    export class MultiSelectDropdownComponent {
      isOpen = false;
    
      options: Option[] = [
        { label: 'React', value: 'react' },
        { label: 'Vue', value: 'vue' },
        { label: 'Svelte', value: 'svelte' },
        { label: 'Solid', value: 'solid' },
        { label: 'Angular', value: 'angular' }
      ];
    
      toggleDropdown(): void {
        this.isOpen = !this.isOpen;
      }
    }
    

    2. Usar *ngFor para renderizar el listado en el HTML

    En multi-select-dropdown.component.html, reemplaza los <li> fijos por:

    <ul class="p-3 space-y-3 text-sm text-gray-700">
      <li *ngFor="let option of options">
        <label class="flex items-center gap-2">
          <input type="checkbox" />
          {{ option.label }}
        </label>
      </li>
    </ul>
    

    ¿Qué logramos?

    • Las opciones ahora se gestionan desde el .ts, de forma escalable.

    • Podemos cambiar las opciones fácilmente o cargarlas desde un API más adelante.

    • El template está más limpio, sin duplicación.

    Paso 4: Manejar selección múltiple con checkboxes

    Queremos que:

    1. Cada vez que se seleccione o deseleccione una opción, actualicemos un array.

    2. El array contenga los value de las opciones seleccionadas.

    3. Usemos console.log() para mostrar el estado en tiempo real.

    ¿Cómo lo haremos?

    • Creamos selected: string[] = [] en el componente.

    • Usamos (change) en cada checkbox para manejar la selección.

    • Usamos [checked] para marcar el estado del checkbox.

    • Imprimimos en consola cada vez que cambia la lista.

    1. Agrega el estado y función en el .ts

    selected: string[] = [];
    
    toggleSelection(value: string): void {
      if (this.selected.includes(value)) {
        this.selected = this.selected.filter(v => v !== value);
      } else {
        this.selected.push(value);
      }
    
      console.log('Seleccionadas:', this.selected);
    }
    

    2. Actualiza el input en el HTML

    Modifica el input dentro del *ngFor así:

    <input
      type="checkbox"
      [checked]="selected.includes(option.value)"
      (change)="toggleSelection(option.value)"
    />
    

    Resultado

    Ahora tu componente:

    • Permite seleccionar múltiples opciones.

    • Guarda la selección actualizada en this.selected.

    • Muestra el array actualizado en consola cada vez que haces clic.

     

    Cierre del post:

    🎯 ¿Qué hicimos?

    Construimos el mismo componente multiselect dropdown visualmente consistente en:

    Framework Enfoque Lenguaje Estado JSX / Template Observaciones
    React Hooks (useState, useEffect) TypeScript useState JSX Familiar y flexible. Control explícito.
    Vue 3 Composition API + ref() + watch TypeScript ref, watch Plantilla declarativa Reactividad clara y directa. Fácil binding.
    Svelte Variables reactivas (let, $:) TypeScript Local vars + auto-reactividad Template HTML puro Sintaxis más limpia y mínima. Muy ergonómico.
    Angular Component Class + *ngIf + (change) TypeScript Clases + propiedades Template estructurado Verboso pero robusto. Ideal en equipos grandes.

    ¿Qué aprendimos?

    • Todos los frameworks modernos permiten construir componentes dinámicos y accesibles.

    • Aunque el resultado visual es el mismo, cada ecosistema tiene su propio “ritmo”:

      • React son ideales si te gusta el control imperativo y JSX.

      • Vue y Svelte son más declarativos y expresivos.

      • Angular prioriza estructura, reglas y escalabilidad.

     

    ¿Cuál fue más simple?

    • En cuanto a cantidad de código y claridad inmediata, Svelte y Vue 3 se sintieron más livianos.

    • React ofrecieron control fino y granularidad, ideales para lógica compleja.

    • Angular fue el más verboso, pero también el más estricto y mantenible para proyectos grandes.

    ¿Dónde usar este patrón?

    Un componente como este es útil en:

    • Formularios de filtro avanzados

    • Paneles de administración

    • Dashboards

    • Apps móviles con diseño adaptativo

    Conclusión

    Construir un componente funcional, accesible y visualmente uniforme en diferentes frameworks es una excelente forma de:

    • Aprender sus fortalezas y diferencias

    • Comparar enfoques reactivos

    • Evaluar la curva de entrada y el esfuerzo de mantenimiento

    Como desarrolladores, saber trabajar en más de un entorno nos hace más versátiles y estratégicos.

  • Arquitectura moderna con microfrontends, monorepo y BFF: Caso real TCG Pocket

    Arquitectura moderna con microfrontends, monorepo y BFF: Caso real TCG Pocket

    Introducción

    En este proyecto construiremos una aplicación real usando datos de cartas del juego Pokémon TCG Pocket, siguiendo una arquitectura moderna basada en:

    • Microfrontends con Next.js y Module Federation

    • Un backend centralizado tipo BFF con NestJS

    • Almacenamiento de imágenes en AWS S3

    • Infraestructura gestionada con GitHub Actions

    • Seguridad, observabilidad, estado compartido y CI/CD listos para producción

    Objetivo del proyecto

    Crear una app web escalable y mantenible, explorando sets y cartas del juego TCG Pocket, aplicando buenas prácticas reales de arquitectura frontend y backend.

    Arquitectura del sistema

    Tecnologías principales:

    Capa Tecnología
    Frontend Next.js + Module Federation
    Estado global Zustand + BroadcastChannel
    Backend BFF NestJS
    Almacenamiento AWS S3 (para imágenes)
    CI/CD GitHub Actions
    Observabilidad Sentry + logs estructurados

    Estructura del monorepo:

    /apps
      /shell            → MFE host (layout, navegación, login)
      /sets             → Microfrontend: exploración de sets
      /cards            → Microfrontend: detalle de cartas
      /admin            → (opcional) manejo de imágenes u orden
      /bff              → Backend For Frontend con NestJS
    
    /libs
      /shared           → Tipos TypeScript, helpers, componentes comunes
      /ui               → Design system / componentes compartidos
    

    Microservicios y contratos

    Usaremos solo un backend (BFF) centralizado que expone:

    Método Ruta Descripción
    GET /sets Lista todos los sets
    GET /sets/:setCode Detalle y cartas de un set
    GET /sets/:setCode/cards/:cardId Detalle completo de una carta específica

    Los datos serán cargados previamente en una base de datos, a partir del scraping ya realizado. El backend BFF accederá a esta base para servir los endpoints públicos de sets y cartas.

    Autenticación (más adelante)

    • OAuth con Google en el frontend

    • Validación en el BFF

    • Emisión de JWT propio

    • Uso de HttpOnly cookie o Authorization header

     

    Almacenamiento y deployment

    • Imágenes ya scrapeadas → se almacenarán en AWS S3 y servidas vía CloudFront

    • Microfrontends: deploy en Vercel

    • Backend: deploy en Render o AWS ECS

    • CI/CD: GitHub Actions con despliegues preview por rama

     

    Observabilidad y métricas

    • Sentry para capturar errores frontend y backend

    • Pino o Winston para logs estructurados en BFF

    • (Opcional) Prometheus/Grafana o Datadog para trazas y rendimiento

     

    Fases del proyecto (posts futuros)

    1. Setup del monorepo con Nx o Turborepo + estructura base

    2. Implementación del shell (layout principal)

    3. Microfrontend de sets

    4. Microfrontend de cartas

    5. Backend BFF con NestJS + rutas

    6. Integración de datos y despliegue del API

    7. Estado global compartido (Zustand + BroadcastChannel)

    8. AWS S3: servir imágenes scrapeadas

    9. OAuth con Google + JWT + roles

    10. CI/CD con GitHub Actions

    11. Observabilidad con Sentry y logs

    12. Extras: favoritos, versión offline (PWA), etc.

     

    Cierre

    Este será el inicio de una serie completa donde aplicamos todas las decisiones arquitectónicas modernas en un proyecto real, usable y extensible. En el próximo post crearemos el monorepo, el shell de layout y comenzaremos a conectar los microfrontends.

  • React 19: Todo acerca de los nuevos Hooks!

    React 19: Todo acerca de los nuevos Hooks!

    React 19 marca un hito importante con la introducción de cinco nuevos hooks diseñados para mejorar la gestión de formularios, el manejo del estado en componentes asincrónicos y la experiencia de desarrollo en general. Estos hooks nos permiten escribir código más limpio, declarativo y reactivo.

    En este artículo analizaremos uno por uno:

    1. Qué problema resuelven

    2. Cómo se resolvía antes

    3. Cómo se resuelve ahora con React 19

    4. Casos reales con ejemplos de código comentado en español

    useOptimistic

    Qué resuelve

    Permite aplicar actualizaciones optimistas en la UI: mostrar un cambio antes de que la operación se confirme en el servidor. Ideal para UX inmediata.

    Antes (React 18 y anteriores)

    "use client";
    
    import { useState } from "react";
    
    export default function TodoList() {
      const [tasks, setTasks] = useState<string[]>([]);
      const [loading, setLoading] = useState(false);
    
      const addTask = async (task: string) => {
        const tempTask = `⌛ ${task}`;
        setTasks((prev) => [...prev, tempTask]);
        setLoading(true);
    
        try {
          await new Promise((r) => setTimeout(r, 1000));
          setTasks((prev) => [...prev.filter(t => t !== tempTask), task]);
        } catch (err) {
          alert("Error");
          setTasks((prev) => prev.filter(t => t !== tempTask));
        } finally {
          setLoading(false);
        }
      };
    
      return (
        <div>
          <ul>{tasks.map((t, i) => <li key={i}>{t}</li>)}</ul>
          <button onClick={() => addTask("Nueva tarea")} disabled={loading}>
            {loading ? "Agregando..." : "Agregar"}
          </button>
        </div>
      );
    }
    

    Ahora con useOptimistic

    "use client";
    
    import { useState, useOptimistic } from "react";
    
    export default function TodoListOptimistic() {
      const [tasks, setTasks] = useState<string[]>([]);
      const [optimisticTasks, addOptimisticTask] = useOptimistic(
        tasks,
        (current, newTask: string) => [...current, `⌛ ${newTask}`]
      );
    
      async function handleAdd(task: string) {
        addOptimisticTask(task); // UI inmediata
        try {
          await new Promise((r) => setTimeout(r, 1000));
          setTasks((prev) => [...prev, task]);
        } catch {
          alert("Fallo en el backend");
        }
      }
    
      return (
        <div>
          <ul>{optimisticTasks.map((t, i) => <li key={i}>{t}</li>)}</ul>
          <button onClick={() => handleAdd("React 19")}>Agregar</button>
        </div>
      );
    }
    

    Ventajas

    • Menos líneas de código

    • Menor estado a manejar

    • Flujo limpio y desacoplado

    • Sin riesgo de olvidar revertir el estado

    useFormStatus

    Qué resuelve

    Este hook permite acceder al estado del formulario (pending, submitted, error) sin necesidad de pasar props manualmente. Se usa dentro de un <form> con action.

    Antes

    "use client";
    
    import { useState } from "react";
    
    function SubmitButton({ loading }: { loading: boolean }) {
      return <button disabled={loading}>{loading ? "Enviando..." : "Enviar"}</button>;
    }
    
    export default function ContactForm() {
      const [loading, setLoading] = useState(false);
    
      const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setLoading(true);
        await new Promise((r) => setTimeout(r, 1000));
        setLoading(false);
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <textarea name="msg" />
          <SubmitButton loading={loading} />
        </form>
      );
    }
    

    Ahora con useFormStatus (React 19)

    "use client";
    
    import { useFormStatus } from "react-dom";
    
    function SubmitButton() {
      const { pending } = useFormStatus();
      return <button disabled={pending}>{pending ? "Enviando..." : "Enviar"}</button>;
    }
    
    export default function ContactForm() {
      async function handleSubmit(formData: FormData) {
        await new Promise((r) => setTimeout(r, 1000));
        console.log("Mensaje:", formData.get("msg"));
      }
    
      return (
        <form action={handleSubmit}>
          <textarea name="msg" />
          <SubmitButton />
        </form>
      );
    }
    

    Ventajas

    • El botón conoce el estado sin necesidad de props

    • Composición más limpia

    • Ideal para formularios reutilizables o componentizados

    useFormState

    Qué resuelve

    useFormState permite manejar el estado de un formulario basado en el resultado de una action, típicamente una server action. Es útil cuando queremos mostrar mensajes de éxito o error sin tener que controlar el estado manualmente en el cliente.

    Antes

    "use client";
    
    import { useState } from "react";
    
    export default function FeedbackForm() {
      const [statusMsg, setStatusMsg] = useState("");
    
      const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        const formData = new FormData(e.currentTarget);
        await new Promise((r) => setTimeout(r, 1000));
        setStatusMsg(`Gracias, ${formData.get("name")}`);
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <input name="name" required />
          <button type="submit">Enviar</button>
          {statusMsg && <p>{statusMsg}</p>}
        </form>
      );
    }
    

    Ahora con useFormState

    "use client";
    
    import { useFormState } from "react-dom";
    
    // Función del lado servidor (puede ir en el mismo archivo o en uno separado)
    async function submitForm(_: string, formData: FormData) {
      const name = formData.get("name")?.toString() ?? "";
      await new Promise((r) => setTimeout(r, 1000));
      return `Gracias, ${name}`;
    }
    
    export default function FeedbackForm() {
      const [message, formAction] = useFormState(submitForm, "");
    
      return (
        <form action={formAction}>
          <input name="name" required />
          <button type="submit">Enviar</button>
          {message && <p>{message}</p>}
        </form>
      );
    }
    

    Ventajas

    • No necesitas manejar useState para el mensaje.

    • El estado viene directamente de la action.

    • Reduce la lógica en el cliente.

    useActionState

    Qué resuelve

    useActionState es similar a useFormState, pero da más control: puedes manejar el estado del formulario, errores y respuesta, todo desde una acción del servidor. Es perfecto para flujos con validaciones o mutaciones complejas.

    Antes (validación manual)

    "use client";
    
    import { useState } from "react";
    
    export default function LoginForm() {
      const [error, setError] = useState("");
    
      const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        const formData = new FormData(e.currentTarget);
        const user = formData.get("user")?.toString();
        const pass = formData.get("pass")?.toString();
    
        if (!user || !pass) {
          setError("Faltan datos");
          return;
        }
    
        await new Promise((r) => setTimeout(r, 1000));
        if (user !== "admin" || pass !== "1234") {
          setError("Credenciales inválidas");
        } else {
          setError("");
          alert("Login exitoso");
        }
      };
    
      return (
        <form onSubmit={handleLogin}>
          <input name="user" placeholder="Usuario" />
          <input name="pass" type="password" placeholder="Contraseña" />
          <button>Iniciar sesión</button>
          {error && <p>{error}</p>}
        </form>
      );
    }
    

    Ahora con useActionState (React 19)

    "use client";
    
    import { useActionState } from "react";
    
    async function loginAction(
      prevState: { error?: string },
      formData: FormData
    ) {
      const user = formData.get("user")?.toString();
      const pass = formData.get("pass")?.toString();
    
      if (!user || !pass) {
        return { error: "Completa ambos campos" };
      }
    
      await new Promise((r) => setTimeout(r, 1000));
    
      if (user !== "admin" || pass !== "1234") {
        return { error: "Credenciales inválidas" };
      }
    
      return { error: undefined };
    }
    
    export default function LoginForm() {
      const [state, formAction] = useActionState(loginAction, { error: undefined });
    
      return (
        <form action={formAction}>
          <input name="user" placeholder="Usuario" />
          <input name="pass" type="password" />
          <button type="submit">Iniciar sesión</button>
          {state.error && <p>{state.error}</p>}
        </form>
      );
    }
    

    Ventajas

    • Toda la lógica de validación se mueve al servidor.

    • El estado de error es limpio y gestionado por React.

    • Facilita construir flujos seguros y controlados.

    use

    Qué resuelve

    use() permite leer el resultado de una promesa directamente desde un componente sin useEffect, ni estados intermedios. Es ideal cuando estás dentro de un React Server Component (RSC) y necesitas datos asincrónicos.

    Antes

    // Client component con useEffect
    "use client";
    import { useEffect, useState } from "react";
    
    export default function UserInfo() {
      const [user, setUser] = useState<{ name: string } | null>(null);
    
      useEffect(() => {
        fetch("/api/user")
          .then(res => res.json())
          .then(setUser);
      }, []);
    
      if (!user) return <p>Cargando...</p>;
      return <p>Hola, {user.name}</p>;
    }
    

    Ahora con use (React Server Components)

    import { use } from "react";
    
    // Función asincrónica que retorna datos
    async function getUser() {
      return { name: "Luis Velito" };
    }
    
    export default function UserInfo() {
      const user = use(getUser()); // Promesa directamente
      return <p>Hola, {user.name}</p>;
    }
    

    Ventajas

    • Código más limpio

    • Sin estados ni efectos para datos asincrónicos

    • Ideal en SSR (Server-Side Rendering)

    Conclusión

    React 19 trae una nueva era de ergonomía y potencia para el desarrollo frontend. Estos hooks no solo reducen el código repetitivo, sino que también nos acercan a una experiencia más declarativa y reactiva, con menos errores y una mejor separación de responsabilidades.

    ¿Qué deberías probar hoy?

    • Refactoriza tus formularios con useFormStatus y useFormState.

    • Usa useOptimistic en interacciones frecuentes como likes, comentarios o toggle de favoritos.

    • Si haces validaciones, migra a useActionState.

    • Si estás usando Server Components, empieza a adoptar use() en llamadas de datos.

     

  • Manejo de estado global con Zustand en Next.js – Login y persistencia

    Manejo de estado global con Zustand en Next.js – Login y persistencia

    Zustand es una librería de manejo de estado global para React, ligera y muy poderosa. En este tutorial vamos a integrarla en un proyecto con Next.js (App Router) para:

    • Simular un login con un endpoint mock

    • Guardar y persistir los datos del usuario

    • Proteger rutas

    • Mostrar la información del usuario en un Header

    • Pasar datos al componente Slider

    Vamos a trabajar directamente sobre un proyecto Next.js ya creado, usando la estructura con app, components, stores, etc.

    Estructura base

    Nuestro árbol de carpetas relevante será:

    src/
    ├── app/
    │   ├── page.tsx         // Login (index)
    │   └── dashboard/
    │       └── page.tsx     // Página interna
    ├── components/
    │   ├── Header.tsx
    │   └── Slider.tsx
    ├── hooks/
    │   └── useUserClient.ts
    ├── mocks/
    │   └── user.ts
    └── stores/
        └── userStore.ts
    

     

    Instalando Zustand

    Si aún no lo tienes instalado:

    npm install zustand

    Y para persistencia:

    npm install zustand/middleware

    1. Mock del endpoint de login

    Creamos un login simulado en src/mocks/user.ts:

    export async function mockLogin(username: string, password: string) {
      await new Promise((r) => setTimeout(r, 500));
    
      if (username === "admin" && password === "1234") {
        return {
          id: 1,
          name: "Luis Velito",
          email: "admin@example.com",
          role: "admin",
          sliderValue: 60,
          sliderNegative: true,
          steps: 10,
          showTooltip: "always",
        };
      }
    
      throw new Error("Invalid credentials");
    }
    

    2. Zustand store con persistencia

    Creamos el store en src/stores/userStore.ts:

    import { create } from "zustand";
    import { persist } from "zustand/middleware";
    
    interface User {
      id: number;
      name: string;
      email: string;
      role: string;
      sliderValue: number;
      sliderNegative: boolean;
      steps: number;
      showTooltip: string;
    }
    
    interface UserStore {
      user: User | null;
      login: (data: User) => void;
      logout: () => void;
    }
    
    export const useUserStore = create<UserStore>()(
      persist(
        (set) => ({
          user: null,
          login: (data) => set({ user: data }),
          logout: () => set({ user: null }),
        }),
        {
          name: "user-storage", // clave en localStorage
        }
      )
    );
    

    3. Hook useUserClient para manejar hidratación

    En Next.js con SSR, hay que esperar a que Zustand se hidrate. Creamos un hook en src/hooks/useUserClient.ts:

    "use client";
    
    import { useEffect, useState } from "react";
    import { useUserStore } from "@/stores/userStore";
    
    export function useUserClient() {
      const user = useUserStore((s) => s.user);
      const [isHydrated, setIsHydrated] = useState(false);
    
      useEffect(() => {
        setIsHydrated(true);
      }, []);
    
      return { user, isHydrated };
    }
    

    4. Página de login (/)

    Usamos el mock de login y guardamos la sesión con Zustand:

    // src/app/page.tsx
    "use client";
    
    import { useEffect, useState } from "react";
    import { useRouter } from "next/navigation";
    import { mockLogin } from "@/mocks/user";
    import { useUserStore } from "@/stores/userStore";
    import { useUserClient } from "@/hooks/useUserClient";
    
    export default function LoginPage() {
      const router = useRouter();
      const login = useUserStore((s) => s.login);
      const { user } = useUserClient();
    
      const [username, setUsername] = useState("");
      const [password, setPassword] = useState("");
      const [error, setError] = useState("");
    
      useEffect(() => {
        if (user) router.push("/dashboard");
      }, [user]);
    
      const handleLogin = async () => {
        try {
          const loggedUser = await mockLogin(username, password);
          login(loggedUser);
        } catch (err) {
          setError("Credenciales inválidas: " + err);
        }
      };
    
      return (
        <div className="flex items-center justify-center min-h-screen min-w-screen bg-gray-200">
          <div className="bg-white p-10 rounded-lg shadow-md text-gray-700">
            <h1 className="text-2xl font-bold mb-4">Iniciar sesión</h1>
            <input
              className="border p-2 w-full mb-2"
              placeholder="Usuario"
              value={username}
              onChange={(e) => setUsername(e.target.value)}
            />
            <input
              className="border p-2 w-full mb-4"
              type="password"
              placeholder="Contraseña"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
            />
            <button className="bg-black text-white px-4 py-2" onClick={handleLogin}>
              Entrar
            </button>
            {error && <p className="text-red-600 mt-2">{error}</p>}
          </div>
        </div>
      );
    }
    

    5. Página interna /dashboard

    Protegida, con redirección si no hay sesión:

    // src/app/dashboard/page.tsx
    "use client";
    
    import Header from "@/components/Header";
    import Slider from "@/components/Slider";
    import { useUserClient } from "@/hooks/useUserClient";
    
    export default function DashboardPage() {
      const { user, isHydrated } = useUserClient();
    
      if (!isHydrated) return null;
      if (!user) return null;
    
      return (
        <div className="min-h-screen min-w-screen bg-gray-200">
          <Header />
          <div className="flex items-center justify-center container mx-auto p-4">
            <div className="bg-white p-8 rounded-lg shadow-md min-w-[600px]">
              <h2 className="text-xl font-semibold mb-12 text-gray-700">Slider personalizado</h2>
              <Slider
                value={user.sliderValue}
                negative={user.sliderNegative}
                steps={user.steps}
                showTooltip={user.showTooltip as "always" | "onInteraction" | undefined}
              />
            </div>
          </div>
        </div>
      );
    }
    

    5. Componente Header con cierre de sesión

    Código del componente header:

    // src/components/Header.tsx
    "use client";
    
    import { useUserClient } from "@/hooks/useUserClient";
    import { useUserStore } from "@/stores/userStore";
    import { useRouter } from "next/navigation";
    import { useEffect } from "react";
    
    export default function Header() {
      const { user, isHydrated } = useUserClient();
      const logout = useUserStore((s) => s.logout);
      const router = useRouter();
    
      useEffect(() => {
        if (isHydrated && !user) {
          router.push("/");
        }
      }, [user, isHydrated]);
    
      if (!isHydrated || !user) return null;
    
      return (
        <header className="flex justify-between items-center p-4 bg-gray-100 rounded-md shadow">
          <div>
            <p className="text-sm text-gray-700">Bienvenido,</p>
            <p className="font-bold text-gray-900 text-lg">{user.name}</p>
          </div>
          <div className="flex flex-col items-end">
            <p className="text-sm text-gray-600">{user.email}</p>
            <p
              className="mt-4 text-red-500 cursor-pointer text-xs hover:text-red-600 uppercase"
              onClick={() => {
                logout();
                router.push("/");
              }}
            >
              Cerrar sesión
            </p>
          </div>
        </header>
      );
    }
    

    Resultado final

    • ✅ Login simulado con Zustand

    • ✅ Datos persistentes entre recargas

    • ✅ Página protegida con redirección

    • ✅ Header con sesión e información

    • ✅ Slider dinámico con props desde el store

    Conclusión

    Zustand es ideal para manejar sesiones de usuario en aplicaciones React y Next.js, sin el peso de soluciones más complejas. Con persist, una estructura clara y algunos hooks de cliente, puedes construir experiencias sólidas y modernas fácilmente.

  • Tutorial: Creando un Componente Slider Personalizado en React

    Tutorial: Creando un Componente Slider Personalizado en React

    Introducción

    En este tutorial, vamos a crear un componente Slider personalizado en React que ofrece funcionalidades avanzadas como soporte para valores negativos, pasos personalizables, tooltips y accesibilidad. Este componente es ideal para aplicaciones que requieren una interfaz de usuario interactiva para seleccionar valores numéricos.

    Requisitos Previos

    • Conocimientos básicos de React
    • Familiaridad con TypeScript
    • Un proyecto React configurado (podemos usar el que creamos en nuestro tutorial: Next Tutorial.)

    Paso 1: Configuración Inicial

    Primero, vamos a crear un nuevo archivo para nuestro componente Slider. En un proyecto Next.js, podríamos colocarlo en src/app/components/slider.tsx/Slider.tsx.
    "use client";
    
    import React, { useState, useRef, useCallback, useMemo, memo, useEffect } from "react";
    El atributo “use client” es necesario en Next.js para indicar que este componente se ejecutará en el lado del cliente. Esto es importante porque nuestro componente utiliza eventos del navegador y manipulación del DOM, que solo están disponibles en el cliente.
    Importamos varios hooks de React:

    • useState: Para gestionar el estado local del componente
    • useRef: Para mantener una referencia al elemento DOM del slider
    • useCallback: Para memorizar funciones y evitar recreaciones innecesarias
    • useMemo: Para memorizar valores calculados
    • memo: Para evitar renderizados innecesarios del componente
    • useEffect: Para manejar efectos secundarios como la inicialización

    Paso 2: Definición de Tipos

    Definimos las interfaces para nuestro componente:
    // Definición de tipos para el componente
    // StepPoint representa un punto de paso en el slider, con un valor numérico y un indicador de si es un valor extremo
    interface StepPoint {
      value: number;
      isExtreme: boolean;
    }
    
    // SliderProps define todas las propiedades que puede recibir nuestro componente Slider
    interface SliderProps {
      /** El valor máximo del slider */
      value: number;
      /** Indica si el slider debe soportar valores negativos */
      negative?: boolean;
      /** El tamaño del paso entre valores */
      steps: number;
      /** Ancho opcional del slider */
      width?: string;
      /** Función de callback cuando el valor cambia */
      onChange?: (value: number) => void;
      /** Valor inicial del slider */
      defaultValue?: number;
      /** Indica si el slider está deshabilitado */
      disabled?: boolean;
      /** Clase CSS personalizada para el contenedor del slider */
      className?: string;
      /** Función para formatear el valor mostrado */
      formatValue?: (value: number) => string;
      /** Modo de visibilidad del tooltip: 'always' | 'onInteraction' */
      showTooltip?: 'always' | 'onInteraction';
    }
    Estas interfaces definen la estructura de datos para los puntos de paso y las propiedades que aceptará nuestro componente.
    • StepPoint: Define la estructura de cada punto de paso en el slider, con un valor numérico y un indicador de si es un valor extremo (mínimo o máximo).
    • SliderProps: Define todas las propiedades que puede recibir nuestro componente Slider, con comentarios JSDoc para documentar cada propiedad.

    Paso 3: Configuración de Props por Defecto

    Establecemos valores predeterminados para las propiedades opcionales:

    // Valores predeterminados para las propiedades opcionales
    const defaultProps: Partial<SliderProps> = {
      negative: false,
      disabled: false,
      showTooltip: 'onInteraction',
      formatValue: (value: number) => value.toString(),
    };
    Estos valores predeterminados se utilizarán cuando no se proporcionen las propiedades correspondientes al componente. Esto hace que el componente sea más fácil de usar, ya que no es necesario especificar todas las propiedades cada vez.

    Paso 4: Implementación del Componente Principal

    Creamos el componente Slider utilizando memo para optimizar el rendimiento:

    // Componente Slider principal
    // Utilizamos memo para evitar renderizados innecesarios cuando las props no cambian
    const Slider = memo(({
      value,
      negative = defaultProps.negative,
      steps: initialSteps,
      onChange,
      defaultValue,
      disabled = defaultProps.disabled,
      className = "",
      formatValue = defaultProps.formatValue,
      showTooltip = defaultProps.showTooltip,
    }: SliderProps) => {
      // Component implementation
    });
    Utilizamos memo para evitar renderizados innecesarios del componente cuando sus props no cambian. Esto es especialmente útil para componentes que se renderizan frecuentemente o que tienen cálculos costosos.
    En la desestructuración de props, asignamos valores predeterminados a las propiedades opcionales utilizando los valores definidos en defaultProps.

    Paso 5: Validación de Props

    Agregamos validaciones para asegurar que los valores proporcionados sean válidos:

    // Validación de props para asegurar que los valores sean válidos
    if (value <= 0) {
      throw new Error("El valor del slider debe ser mayor que 0");
    }
    if (initialSteps <= 0) {
      throw new Error("Los pasos del slider deben ser mayores que 0");
    }
    if (initialSteps > value) {
      console.warn("Los pasos del slider no pueden ser mayores que el valor. Usando el valor como pasos.");
    }
    
    // Aseguramos que los pasos no sean mayores que el valor
    const steps = Math.min(initialSteps, value);
    Estas validaciones garantizan que el componente reciba valores válidos y proporcione mensajes de error claros cuando no sea así. También ajustamos automáticamente el valor de steps si es mayor que value, para evitar comportamientos inesperados.

    Paso 6: Estado y Referencias

    Configuramos el estado y las referencias necesarias:

    // Estado del componente
    // mounted: indica si el componente está montado (para evitar problemas de hidratación)
    // position: posición actual del thumb en porcentaje (0-100)
    // currentValue: valor numérico actual del slider
    // isDragging: indica si el usuario está arrastrando el thumb
    // sliderRef: referencia al elemento DOM del slider
    const [mounted, setMounted] = useState(false);
    const [position, setPosition] = useState(0);
    const [currentValue, setCurrentValue] = useState(0);
    const [isDragging, setIsDragging] = useState(false);
    const sliderRef = useRef<HTMLDivElement>(null);
    • mounted: Un estado booleano que indica si el componente está montado. Esto es útil para evitar problemas de hidratación en Next.js.
    • position: La posición actual del thumb del slider en porcentaje (0-100).
    • currentValue: El valor numérico actual del slider.
    • isDragging: Un estado booleano que indica si el usuario está arrastrando el thumb del slider.
    • sliderRef: Una referencia al elemento DOM del slider, que utilizamos para calcular posiciones relativas.

    Paso 7: Inicialización del Estado

    Utilizamos useEffect para inicializar el estado después del montaje del componente:
    // Inicialización del estado después del montaje del componente
    // Esto evita problemas de hidratación en Next.js
    useEffect(() => {
      setMounted(true);
      
      // Calculamos la posición inicial del thumb basada en el valor predeterminado
      const initialPosition = defaultValue !== undefined
        ? negative
          ? ((defaultValue + value) / (value * 2)) * 100 // Para valores negativos, el 0 está en el medio (50%)
          : (defaultValue / value) * 100 // Para valores positivos, el 0 está al principio (0%)
        : negative ? 50 : 0; // Si no hay valor predeterminado, usamos 50% para negativos o 0% para positivos
    
      // Calculamos el valor inicial, asegurándonos de que esté dentro del rango permitido
      const initialValue = defaultValue !== undefined
        ? Math.max(
            negative ? -value : 0,
            Math.min(value, defaultValue)
          )
        : negative ? 0 : 0;
    
      // Actualizamos el estado con los valores calculados
      setPosition(initialPosition);
      setCurrentValue(initialValue);
    }, [defaultValue, value, negative]);
    Este efecto se ejecuta después del montaje del componente y cuando cambian defaultValuevalue o negative. Calcula la posición inicial del thumb y el valor inicial del slider basándose en las props proporcionadas.
    La fórmula para calcular initialPosition es diferente dependiendo de si el slider soporta valores negativos o no:

    • Si negative es true, el rango va de -value a value, por lo que el valor 0 está en el medio (50%).
    • Si negative es false, el rango va de 0 a value, por lo que el valor 0 está al principio (0%).

    Paso 8: Cálculo de Puntos de Paso

    Utilizamos useMemo para calcular los puntos de paso de manera eficiente:

    // Cálculo de los puntos de paso
    // Utilizamos useMemo para evitar recálculos innecesarios
    const stepPoints = useMemo(() => {
      const points: StepPoint[] = [];
    
      if (negative) {
        // Para valores negativos, generamos puntos desde -value hasta value
        const totalSteps = Math.floor((value * 2) / steps);
        
        // Punto mínimo (-value)
        points.push({ value: -value, isExtreme: true });
    
        // Puntos negativos intermedios
        for (let i = 1; i < totalSteps / 2; i++) {
          points.push({ value: -value + i * steps, isExtreme: false });
        }
    
        // Punto cero
        points.push({ value: 0, isExtreme: true });
    
        // Puntos positivos intermedios
        for (let i = 1; i < totalSteps / 2; i++) {
          points.push({ value: i * steps, isExtreme: false });
        }
    
        // Punto máximo (value)
        points.push({ value: value, isExtreme: true });
      } else {
        // Para valores positivos, generamos puntos desde 0 hasta value
        const totalSteps = Math.floor(value / steps);
        
        // Punto mínimo (0)
        points.push({ value: 0, isExtreme: true });
    
        // Puntos intermedios
        for (let i = 1; i < totalSteps; i++) {
          points.push({ value: i * steps, isExtreme: false });
        }
    
        // Punto máximo (value)
        points.push({ value: value, isExtreme: true });
      }
    
      return points;
    }, [value, steps, negative]);
    Este cálculo se realiza solo cuando cambian valuesteps o negative, gracias a useMemo. Genera un array de puntos de paso que se utilizarán para renderizar las marcas en el slider.
    La lógica es diferente dependiendo de si el slider soporta valores negativos o no:

    • Si negative es true, generamos puntos desde -value hasta value, con 0 en el medio.
    • Si negative es false, generamos puntos desde 0 hasta value.
    Los valores extremos (mínimo, cero y máximo) se marcan con isExtreme: true para que se puedan renderizar de manera diferente.

    Paso 9: Lógica de Actualización de Posición

    Implementamos la función para actualizar la posición del slider:

    // Función para actualizar la posición del slider
    // Se llama cuando el usuario interactúa con el slider
    const updatePosition = useCallback((clientX: number) => {
      // Si el slider está deshabilitado o no tenemos referencia al elemento DOM, no hacemos nada
      if (!sliderRef.current || disabled) return;
    
      // Calculamos la posición relativa del cursor dentro del slider
      const rect = sliderRef.current.getBoundingClientRect();
      const x = clientX - rect.left;
      const rawPosition = (x / rect.width) * 100;
      
      // Aseguramos que la posición esté entre 0 y 100
      const clampedPosition = Math.max(0, Math.min(100, rawPosition));
    
      // Calculamos el valor numérico correspondiente a esta posición
      let newValue: number;
      if (negative) {
        // Para valores negativos, el rango va de -value a value
        const range = value * 2;
        newValue = -value + range * (clampedPosition / 100);
      } else {
        // Para valores positivos, el rango va de 0 a value
        newValue = value * (clampedPosition / 100);
      }
    
      // Redondeamos el valor al paso más cercano
      let roundedValue = Math.round(newValue / steps) * steps;
    
      // Aseguramos que el valor esté dentro del rango permitido
      if (negative) {
        roundedValue = Math.max(-value, Math.min(value, roundedValue));
      } else {
        roundedValue = Math.max(0, Math.min(value, roundedValue));
      }
    
      // Calculamos la posición final del thumb basada en el valor redondeado
      let newPosition: number;
      if (negative) {
        newPosition = ((roundedValue + value) / (value * 2)) * 100;
      } else {
        newPosition = (roundedValue / value) * 100;
      }
    
      // Actualizamos el estado y llamamos al callback onChange si está definido
      setPosition(newPosition);
      setCurrentValue(roundedValue);
      onChange?.(roundedValue);
    }, [negative, value, steps, disabled, onChange]);
    Esta función se llama cuando el usuario interactúa con el slider (arrastrando, haciendo clic, etc.). Calcula la nueva posición y valor del slider basándose en la posición del cursor.
    1. Primero, verificamos si el slider está deshabilitado o si no tenemos una referencia al elemento DOM.
    1. Calculamos la posición relativa del cursor dentro del slider.
    1. Convertimos esta posición a un porcentaje (0-100).
    1. Calculamos el valor numérico correspondiente a esta posición, teniendo en cuenta si el slider soporta valores negativos o no.
    1. Redondeamos el valor al paso más cercano.
    1. Aseguramos que el valor esté dentro del rango permitido.
    1. Calculamos la posición final del thumb basándose en el valor redondeado.
    1. Actualizamos el estado y llamamos al callback onChange si está definido.
    Utilizamos useCallback para memorizar esta función y evitar recreaciones innecesarias.

    Paso 10: Manejadores de Eventos

    Implementamos los manejadores para eventos de mouse, touch y teclado:

    // Manejadores de eventos de mouse
    const handleMouseDown = useCallback((e: React.MouseEvent) => {
      if (disabled) return;
      setIsDragging(true);
      updatePosition(e.clientX);
    }, [disabled, updatePosition]);
    
    const handleMouseMove = useCallback((e: React.MouseEvent) => {
      if (!isDragging || disabled) return;
      updatePosition(e.clientX);
    }, [isDragging, disabled, updatePosition]);
    
    const handleMouseUp = useCallback(() => {
      setIsDragging(false);
    }, []);
    
    // Manejadores de eventos táctiles
    const handleTouchStart = useCallback((e: React.TouchEvent) => {
      if (disabled) return;
      setIsDragging(true);
      updatePosition(e.touches[0].clientX);
    }, [disabled, updatePosition]);
    
    const handleTouchMove = useCallback((e: React.TouchEvent) => {
      if (!isDragging || disabled) return;
      updatePosition(e.touches[0].clientX);
    }, [isDragging, disabled, updatePosition]);
    
    const handleTouchEnd = useCallback(() => {
      setIsDragging(false);
    }, []);
    
    // Manejador de eventos de teclado
    // Permite navegar por los valores usando las teclas de flecha
    const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
      if (disabled) return;
    
      const step = steps;
      let newValue = currentValue;
    
      switch (e.key) {
        case "ArrowRight":
        case "ArrowUp":
          // Incrementar el valor
          newValue = Math.min(value, currentValue + step);
          break;
        case "ArrowLeft":
        case "ArrowDown":
          // Decrementar el valor
          newValue = Math.max(negative ? -value : 0, currentValue - step);
          break;
        default:
          return;
      }
    
      // Calculamos la nueva posición basada en el nuevo valor
      const newPosition = negative
        ? ((newValue + value) / (value * 2)) * 100
        : (newValue / value) * 100;
    
      // Actualizamos el estado y llamamos al callback onChange
      setPosition(newPosition);
      setCurrentValue(newValue);
      onChange?.(newValue);
    }, [currentValue, value, steps, negative, disabled, onChange]);
    Estos manejadores de eventos permiten al usuario interactuar con el slider de varias maneras:
    1. Eventos de Mouse:
    • handleMouseDown: Se llama cuando el usuario hace clic en el thumb del slider. Activa el modo de arrastre y actualiza la posición.
    • handleMouseMove: Se llama cuando el usuario mueve el mouse mientras arrastra el thumb. Actualiza la posición si el modo de arrastre está activo.
    • handleMouseUp: Se llama cuando el usuario suelta el botón del mouse. Desactiva el modo de arrastre.
    1. Eventos de Touch:
    • handleTouchStart: Similar a handleMouseDown, pero para eventos táctiles.
    • handleTouchMove: Similar a handleMouseMove, pero para eventos táctiles.
    • handleTouchEnd: Similar a handleMouseUp, pero para eventos táctiles.
    1. Eventos de Teclado:
    • handleKeyDown: Se llama cuando el usuario presiona una tecla mientras el slider tiene el foco. Permite navegar por los valores usando las teclas de flecha.
    Todos estos manejadores utilizan useCallback para memorizar las funciones y evitar recreaciones innecesarias.

    Paso 11: Función para Ajustar el Valor

    Agregamos una función para incrementar o decrementar el valor:
    // Función para incrementar o decrementar el valor
    // Se llama cuando el usuario hace clic en los botones de flecha
    const adjustValue = useCallback((increment: boolean) => {
      if (disabled) return;
      
      const step = steps;
      let newValue = currentValue;
      
      if (increment) {
        // Incrementar el valor
        newValue = Math.min(value, currentValue + step);
      } else {
        // Decrementar el valor
        newValue = Math.max(negative ? -value : 0, currentValue - step);
      }
      
      // Calculamos la nueva posición basada en el nuevo valor
      const newPosition = negative
        ? ((newValue + value) / (value * 2)) * 100
        : (newValue / value) * 100;
      
      // Actualizamos el estado y llamamos al callback onChange
      setPosition(newPosition);
      setCurrentValue(newValue);
      onChange?.(newValue);
    }, [currentValue, value, steps, negative, disabled, onChange]);
    Esta función se llama cuando el usuario hace clic en los botones de incremento o decremento. Ajusta el valor del slider en incrementos de steps y actualiza la posición del thumb en consecuencia.

    Paso 12: Renderizado del Componente

    Finalmente, implementamos el JSX para renderizar nuestro componente:

    // Renderizado del componente
    return (
      // Contenedor principal del slider
      // Utilizamos clases de Tailwind CSS para el estilo
      <div className={`w-[calc(100%-40px)] h-[30px] overflow-visible my-6 ${className}`}>
        <div className="relative w-full">
          {/* Botón de flecha izquierda para decrementar el valor */}
          <button
            type="button"
            className={`absolute left-0 top-1/2 transform -translate-y-1/2 w-6 h-6 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 z-30 ${
              disabled || (negative ? currentValue <= -value : currentValue <= 0) ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
            }`}
            onClick={() => adjustValue(false)}
            disabled={disabled || (negative ? currentValue <= -value : currentValue <= 0)}
            aria-label="Decrease value"
          >
            <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-700" viewBox="0 0 20 20" fill="currentColor">
              <path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
            </svg>
          </button>
    
          {/* Contenedor del slider */}
          <div
            ref={sliderRef}
            className="w-[calc(100%-72px)] h-[30px] overflow-visible mx-auto my-0 relative"
            onMouseMove={handleMouseMove}
            onMouseUp={handleMouseUp}
            onMouseLeave={handleMouseUp}
            onTouchMove={handleTouchMove}
            onTouchEnd={handleTouchEnd}
            onKeyDown={handleKeyDown}
          >
            {/* Pista del slider */}
            <div 
              className={`w-full h-1.5 transform -translate-y-1/2 overflow-visible mx-0 my-[15px] rounded-sm bg-gray-400 z-20 border border-white absolute transition-colors duration-200 ${
                disabled ? 'opacity-50' : ''
              }`}
            />
    
            {/* Marcas de paso */}
            {mounted && stepPoints.map((point, index) => {
              // Calculamos la posición de cada marca basada en su valor
              const linePosition = negative
                ? ((point.value + value) / (value * 2)) * 100
                : (point.value / value) * 100;
    
              return (
                <div
                  key={index}
                  className={`absolute h-${
                    point.isExtreme ? "[24px]" : "[12px]"
                  } w-[1px] bg-gray-400 transition-opacity duration-200 ${
                    disabled ? 'opacity-50' : ''
                  }`}
                  style={{
                    left: `${linePosition}%`,
                    top: point.isExtreme ? "3px" : "9px",
                    height: point.isExtreme ? "24px" : "12px",
                    transform: "translateX(-50%)",
                    zIndex: "10",
                  }}
                />
              );
            })}
    
            {/* Tooltip que muestra el valor actual */}
            {mounted && showTooltip && (
              <div
                className={`absolute bg-blue-500 text-white text-xs px-2 py-1 rounded transform -translate-x-1/2 -translate-y-full -mt-2 z-50 select-none transition-opacity duration-200 ${
                  showTooltip === 'always' || isDragging ? 'opacity-100' : 'opacity-0'
                }`}
                style={{ left: `${position}%` }}
              >
                {formatValue ? formatValue(currentValue) : currentValue.toString()}
                <div className="absolute w-0 h-0 border-l-[6px] border-r-[6px] border-t-[6px] border-l-transparent border-r-transparent border-t-blue-500 left-1/2 transform -translate-x-1/2 bottom-[-6px]" />
              </div>
            )}
    
            {/* Thumb del slider (el elemento que el usuario puede arrastrar) */}
            {mounted && (
              <div
                className={`w-4 h-4 border-2 border-blue-300 z-50 overflow-hidden my-[7px] transform rounded-full bg-blue-400 absolute transition-all duration-200 ${
                  disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'
                }`}
                style={{ 
                  left: `${position}%`,
                  transform: `translateX(-50%) scale(${isDragging ? 1.2 : 1})`
                }}
                onMouseDown={handleMouseDown}
                onTouchStart={handleTouchStart}
              />
            )}
    
            {/* Etiquetas que muestran los valores mínimo y máximo */}
            {mounted && (
              <div className="numbers w-[calc(100%+12px)] h-5 mt-[30px] ml-[-6px] absolute flex justify-between text-black text-xs">
                {negative ? (
                  <>
                    <span>-{value}</span>
                    <span>0</span>
                    <span>{value}</span>
                  </>
                ) : (
                  <>
                    <span>0</span>
                    <span>{value}</span>
                  </>
                )}
              </div>
            )}
          </div>
    
          {/* Botón de flecha derecha para incrementar el valor */}
          <button
            type="button"
            className={`absolute right-0 top-1/2 transform -translate-y-1/2 w-6 h-6 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 z-30 ${
              disabled || currentValue >= value ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
            }`}
            onClick={() => adjustValue(true)}
            disabled={disabled || currentValue >= value}
            aria-label="Increase value"
          >
            <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-700" viewBox="0 0 20 20" fill="currentColor">
              <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
            </svg>
          </button>
        </div>
      </div>
    );
    El JSX está estructurado de la siguiente manera:
    1. Contenedor Principal: Un div que contiene todo el componente, con clases para el ancho, alto y márgenes.
    1. Contenedor Relativo: Un div con posición relativa que contiene los botones de flecha y el slider.
    1. Botón de Flecha Izquierda: Un botón para decrementar el valor del slider. Se deshabilita cuando el valor actual es el mínimo.
    1. Contenedor del Slider: Un div que contiene la pista del slider, las marcas de paso, el tooltip, el thumb y las etiquetas.
    • Pista del Slider: Un div que representa la pista del slider.
    • Marcas de Paso: Se renderizan dinámicamente basándose en stepPoints.
    • Tooltip: Muestra el valor actual y se puede configurar para estar siempre visible o solo durante la interacción.
    • Thumb del Slider: El elemento que el usuario puede arrastrar para cambiar el valor.
    • Etiquetas: Muestran los valores mínimo y máximo del slider.
    1. Botón de Flecha Derecha: Un botón para incrementar el valor del slider. Se deshabilita cuando el valor actual es el máximo.
    Utilizamos clases de Tailwind CSS para estilizar el componente, lo que nos permite crear un diseño responsivo y atractivo sin necesidad de CSS personalizado.

    Paso 13: Exportación del Componente

    Finalmente, exportamos nuestro componente:

    // Establecemos el nombre del componente para mejorar la depuración
    Slider.displayName = 'Slider';
    
    export default Slider;
    Establecemos displayName para mejorar la depuración en las herramientas de desarrollo de React.

    Uso del Componente

    Ahora puedes utilizar el componente Slider en tu aplicación:

    import Slider from './components/slider.tsx/Slider';
    
    function App() {
      const handleChange = (value) => {
        console.log('Slider value changed:', value);
      };
    
      return (
        <div className="p-8">
          <h1 className="text-2xl font-bold mb-4">Mi Slider Personalizado</h1>
          <Slider 
            value={100}
            steps={10}
            negative={true}
            onChange={handleChange}
            defaultValue={0}
            showTooltip="always"
            formatValue={(value) => `${value}%`}
          />
        </div>
      );
    }

    Explicación Detallada de la Estructura HTML

    La estructura HTML del componente está diseñada para proporcionar una experiencia de usuario intuitiva y accesible:
    1. Estructura General:
    • Utilizamos un contenedor principal con ancho calculado (w-[calc(100%-40px)]) para dejar espacio para los botones de flecha.
    • El contenedor tiene overflow-visible para permitir que el tooltip y las etiquetas se muestren fuera del contenedor.
    1. Botones de Flecha:
    • Los botones de flecha están posicionados absolutamente en los extremos del slider.
    • Utilizamos SVG para los iconos de flecha, lo que proporciona una mejor calidad visual y escalabilidad.
    • Los botones se deshabilitan cuando el valor actual es el mínimo o máximo, respectivamente.
    1. Contenedor del Slider:
    • El contenedor del slider tiene un ancho calculado (w-[calc(100%-72px)]) para dejar espacio para los botones de flecha.
    • Tiene overflow-visible para permitir que el tooltip y las etiquetas se muestren fuera del contenedor.
    • Los eventos de mouse, touch y teclado se manejan en este contenedor.
    1. Pista del Slider:
    • La pista del slider es un div con altura fija y bordes redondeados.
    • Tiene un borde blanco para mejorar la visibilidad.
    1. Marcas de Paso:
    • Las marcas de paso se renderizan dinámicamente basándose en stepPoints.
    • Las marcas extremas (mínimo, cero y máximo) son más altas que las marcas intermedias.
    • La posición de cada marca se calcula basándose en su valor.
    1. Tooltip:
    • El tooltip muestra el valor actual del slider.
    • Tiene un fondo azul y texto blanco para destacar.
    • Incluye una flecha que apunta al thumb del slider.
    • La opacidad del tooltip se controla basándose en showTooltip y isDragging.
    1. Thumb del Slider:
    • El thumb es un div circular con un borde y un fondo.
    • Su posición se controla mediante la propiedad left en el estilo.
    • Se escala ligeramente cuando el usuario lo arrastra para proporcionar retroalimentación visual.
    • Los eventos de mouse y touch se manejan en el thumb.
    1. Etiquetas:
    • Las etiquetas muestran los valores mínimo y máximo del slider.
    • Si el slider soporta valores negativos, también se muestra el valor cero en el medio.

    Características del Componente

    1. Soporte para valores negativos: El componente puede mostrar un rango que incluye valores negativos, lo que es útil para aplicaciones que requieren seleccionar valores tanto positivos como negativos.
    1. Pasos personalizables: El componente permite definir el tamaño de los pasos entre valores, lo que es útil para aplicaciones que requieren una selección precisa.
    1. Tooltips: El componente muestra el valor actual en un tooltip que puede estar siempre visible o solo durante la interacción, lo que mejora la experiencia del usuario.
    1. Accesibilidad: El componente es compatible con navegación por teclado y lectores de pantalla, lo que lo hace accesible para todos los usuarios.
    1. Diseño responsivo: El componente se adapta al ancho del contenedor, lo que lo hace ideal para aplicaciones responsivas.
    1. Personalización visual: El componente acepta clases CSS personalizadas para estilizar el componente según las necesidades del usuario.
    1. Soporte para dispositivos táctiles: El componente funciona correctamente en dispositivos móviles, lo que lo hace ideal para aplicaciones multiplataforma.

    Conclusión

    Hemos creado un componente Slider personalizado en React con TypeScript que ofrece una experiencia de usuario rica y accesible. Este componente puede ser utilizado en diversas aplicaciones donde se requiera una selección de valores numéricos con una interfaz intuitiva.
    El componente está diseñado para ser flexible y personalizable, permitiendo a los desarrolladores adaptarlo a sus necesidades específicas. La estructura HTML está optimizada para proporcionar una experiencia de usuario intuitiva y accesible, mientras que la lógica de JavaScript maneja la interacción del usuario y actualiza el estado del componente de manera eficiente.
    Puedes personalizar aún más este componente según tus necesidades específicas, como cambiar los colores, tamaños o agregar funcionalidades adicionales.

     

  • Por qué deberías usar actualizaciones funcionales con setState en React

    Por qué deberías usar actualizaciones funcionales con setState en React

    En React, cuando necesitas actualizar un estado basado en su valor anterior — como en casos reales de gestión de un carrito de compras o una lista de tareas — es fundamental usar la forma funcional de setState para evitar errores sutiles causados por estados desactualizados.

    React agrupa (batch) las actualizaciones de estado para optimizar el rendimiento.
    Si llamas a setState varias veces dentro del mismo ciclo de evento sin usar la forma funcional, es probable que cada llamada trabaje sobre una versión antigua del estado.

    Ejemplo 1: Agregar productos a un carrito

    Incorrecto:

    setCart([...cart, newItem]);

    Correcto:

    setCart(prevCart => [...prevCart, newItem]);

    Ejemplo 2: Incrementar contadores

    Incorrecto:

    setQuantity(quantity + 1);
    setQuantity(quantity + 1);

    Esperado: +2
    Realidad: +1

    Correcto:

    setQuantity(prevQuantity => prevQuantity + 1);
    setQuantity(prevQuantity => prevQuantity + 1);

    ¿Por qué importa?
    En aplicaciones reales — especialmente cuando manejas actualizaciones concurrentes, patrones de UI optimista o interacciones de alta frecuencia — no usar la forma funcional puede provocar pérdida de datos, estados inconsistentes y bugs muy difíciles de rastrear.

    La actualización funcional garantiza que setState siempre trabaje sobre el estado más reciente y correcto, independientemente de las renderizaciones o del agrupamiento de actualizaciones.

    Regla de oro:
    Si tu nuevo estado depende del anterior, siempre usa la forma funcional.

  • ¡Configura tu terminal con OhMyZSH como los más capos!

    ¡Configura tu terminal con OhMyZSH como los más capos!

    ¿Cansado del terminal por defecto en macOS? Personalizarlo no solo mejora la estética, sino que también incrementa tu productividad.

    Z Shell (ZSH) es una poderosa shell de Unix basada en Bash (la shell predeterminada de macOS), pero con muchísimas mejoras.

    En esta guía, configuraremos iTerm2 junto con ZSH y algunos complementos esenciales para lograr un terminal espectacular.

    Temas que cubriremos:

    • Instalación de Homebrew

    • Instalación de iTerm2

    • Instalación de ZSH y Oh My ZSH

    • Configuración de temas, fuentes y plugins para un terminal potente

    Paso 1: Instalar Homebrew

    Homebrew es un gestor de paquetes para macOS que facilita la instalación de software.

    Primero, instala las herramientas de línea de comandos de Xcode ejecutando:

    xcode-select --install

    Si ves un error, reinicia la configuración de Xcode con:

    xcode-select -r

    Luego instala Homebrew con:

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    Paso 2: Instalar iTerm2

    iTerm2 es una alternativa moderna al Terminal de macOS, repleta de funcionalidades avanzadas.

    Instálalo usando Homebrew:

    brew install --cask iterm2

    Paso 3: Instalar ZSH

    macOS ya trae ZSH instalado en /bin/zsh, pero conviene actualizarlo con Brew:

    brew install zsh

    Paso 4: Instalar Oh My Zsh

    Oh My Zsh es un marco de configuración de ZSH impulsado por la comunidad. Hace que la personalización de ZSH sea simple y poderosa.

    Instálalo con:

    sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

    Verifica la versión instalada:

    zsh --version

    Y, si deseas actualizar en el futuro:

    upgrade_oh_my_zsh

    ¡Reinicia iTerm2 para comenzar a usar tu nueva configuración de ZSH!

    Paso 5: Cambiar el tema por defecto

    Oh My Zsh incluye muchos temas. El tema predeterminado es robbyrussell, pero puedes cambiarlo fácilmente.

    Para editar tu configuración:

    nano ~/.zshrc

    O usa tu editor favorito:

    cursor ~/.zshrc

    Busca la línea que dice:

    ZSH_THEME="robbyrussell"

    y cámbiala, por ejemplo, a:

    ZSH_THEME="agnoster"

    Guarda los cambios y recarga la configuración:

    source ~/.zshrc

    Usar un tema personalizado (Ejemplo: Powerlevel9k)

    Si deseas un tema no preinstalado:

    git clone https://github.com/bhilburn/powerlevel9k.git ~/.oh-my-zsh/custom/themes/powerlevel9k
    

    Luego actualiza tu .zshrc:

    ZSH_THEME="powerlevel9k/powerlevel9k"
    

    Y recarga:

    source ~/.zshrc

    Paso 6: Instalar fuentes para Powerline

    Algunos temas (como Powerlevel9k) requieren fuentes especiales (Powerline Fonts).

    Instálalas ejecutando:

    git clone https://github.com/powerline/fonts.git --depth=1
    cd fonts
    ./install.sh
    

    Luego, en iTerm2 ve a: Preferences > Profiles > Text > Change Font
    y selecciona una fuente como Inconsolata o FiraCode (esta última soporta ligaduras).

    Paso 7: Instalar esquemas de colores

    Personaliza aún más tu terminal instalando un esquema de colores:

    1. Descarga los esquemas desde iTerm2 Color Schemes.

    2. Extrae el ZIP.

    3. En iTerm2, ve a:
      Preferences > Profiles > Colors > Color Presets > Import

    4. Importa los archivos .itermcolors desde la carpeta descargada.

    Paso 8: Instalar plugins para ZSH

    Oh My Zsh viene con varios plugins (como git), pero puedes agregar más.

    Por ejemplo, para agregar soporte a Docker:

    git clone https://github.com/zsh-users/zsh-docker.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-docker
    

    Luego edita ~/.zshrc y añade zsh-docker en la sección de plugins:

    plugins=(git zsh-docker)

    Recarga:

    source ~/.zshrc

    Paso 9: Agregar alias personalizados

    Los alias son comandos abreviados. Puedes agregarlos en tu .zshrc.

    Por ejemplo:

    alias dckimgs="docker images"
    

    Así, cada vez que escribas dckimgs verás el listado de imágenes de Docker.

    ¡Listo!

    Ahora tienes un terminal hermoso, funcional y altamente productivo en macOS, listo para acompañarte en todos tus proyectos.

    Si conoces otros trucos o configuraciones útiles para ZSH, ¡Contáctame!

  • Configura Next.js + TailwindCSS + TypeScript + Storybook

    Configura Next.js + TailwindCSS + TypeScript + Storybook

    En este tutorial aprenderás a:

    • Crear un proyecto con Next.js + TypeScript.
    • Integrar TailwindCSS para estilos rápidos y potentes.
    • Instalar Storybook para documentar y testear componentes UI.
    • Hacer que TailwindCSS funcione dentro de Storybook.

    1. Crear el Proyecto Next.js con TypeScript

    Primero, genera un nuevo proyecto Next.js usando TypeScript:

    npx create-next-app@latest my-next-project --typescript
    cd my-next-project

    Te va a dar opciones a escoger para ciertas configuraciones del proyecto:

    • ESLint
    • TailwindCSS
    • Directorio SRC
    • App Router
    • TurboPack
    • Import Alias

    Luego de esas respuestas va a instalar las dependencias necesarias e ingresar en el folder del proyecto.

    2. Probar que Tailwind Funciona

    Abre src/app/page.tsx y reemplázalo por:

    export default function Home() {
      return (
        <div className="flex items-center justify-center min-h-screen bg-gradient-to-r from-purple-400 via-pink-500 to-red-500">
          <h1 className="text-5xl font-bold text-white">¡Hola Next.js + Tailwind!</h1>
        </div>
      )
    }

    Levanta el servidor de desarrollo:

    npm run dev

    Deberías ver un fondo degradado y un título blanco centrado.

    3. Instalar Storybook

    Ahora agregamos Storybook para documentar los componentes:

    npx storybook init --builder webpack5

    Esto instalará Storybook y creará la carpeta .storybook/.

    4. Integrar TailwindCSS en Storybook

    Para que Storybook también use Tailwind, importa el CSS global en el archivo .storybook/preview.ts:

    import '../styles/globals.css'; // Importa estilos de Tailwind
    import type { Preview } from "@storybook/react";
    
    const preview: Preview = {
      parameters: {
        controls: {
          matchers: {
            color: /(background|color)$/i,
            date: /Date$/,
          },
        },
      },
    };
    
    export default preview;

    Con esto, todos tus componentes en Storybook podrán usar clases de TailwindCSS.

    5. Levantar Storybook

    Corre Storybook:

    npm run storybook

    Se abrirá automáticamente en http://localhost:6006.

    Ahora puedes empezar a crear components y stories usando TailwindCSS.

    🚀 Próximos pasos

    • Crear componentes en components/.
    • Escribir archivos .stories.tsx para cada componente.
    • Organizar las historias en “Atoms”, “Molecules” y “Organisms” (arquitectura Atomic Design).