Tag: recommended

  • 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.

  • 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.

  • 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.

  • Los mejores UI kits gratuitos con Tailwind CSS

    Los mejores UI kits gratuitos con Tailwind CSS

    Aquí tienes un resumen rápido de las 10 mejores opciones:

    • Flowbite: Más de 400 componentes, compatible con Laravel y Vue.js
    • DaisyUI: Más de 50 componentes, sintaxis sencilla, más de 15,000 estrellas en GitHub
    • Preline UI: Más de 300 componentes, 160 páginas de inicio, soporte para varios frameworks
    • HyperUI: Más de 42 componentes enfocados en marketing, eCommerce y aplicaciones
    • TailGrids: Más de 300 componentes, compatible con React, Vue y Angular
    • Headless UI: Componentes accesibles sin estilos para control total
    • Tailblocks: Componentes prediseñados para ensamblado rápido de páginas
    • Meraki UI: 108 componentes, soporte para modo oscuro y RTL (idiomas de derecha a izquierda)
    • Mamba UI: Mezcla de componentes gratuitos y de pago, diseños modernos
    • Tailwind Starter Kit: Elementos HTML y componentes dinámicos para múltiples frameworks

    Comparación rápida:

     

    Librería Componentes Estrellas GitHub Soporte de Framework Personalización
    Flowbite 400+ 1,076 Alpine.js, Laravel, Vue.js Alta
    DaisyUI 56 28,494 Cualquier compatible con Tailwind Alta
    Preline UI 300+ 3,200 React, Next.js, Svelte, Remix Alta
    HyperUI 42+ 6,949 Cualquier compatible con Tailwind Moderada
    TailGrids 300+ N/D React, Vue, Angular Alta
    Headless UI Sin estilo 17,300 React, Vue Alta
    Tailblocks Variado N/D Cualquier compatible con Tailwind Limitada
    Meraki UI 108 N/D Cualquier compatible con Tailwind Alta
    Mamba UI 150+ 300 Angular, React, Svelte, Vue Moderada
    Tailwind Starter Kit 120+ 5,000 React, Vue, Angular Alta

    Elige basándote en tus necesidades:

    Cantidad de componentes, compatibilidad con frameworks y nivel de personalización.
    Para muchos componentes, prueba Flowbite o Preline UI.
    ¿Buscas gran comunidad? DaisyUI es la opción.
    ¿Necesitas accesibilidad? Explora Headless UI.

    ¿Cómo elegimos estas herramientas?

    Tomamos en cuenta:

    1. Gratuidad: 100% gratuitas.

    2. Cantidad de componentes: como Flowbite (400+) o TailGrids (300+).

    3. Compatibilidad con frameworks: Flowbite trabaja con Laravel, React y Vue.

    4. Facilidad de uso: DaisyUI destaca con su sintaxis simple.

    5. Personalización: Meraki UI ofrece flexibilidad con Flexbox y CSS Grid.

    6. Actualización: Todos funcionan con la última versión de Tailwind CSS.

    7. Apoyo comunitario: Preferimos librerías open-source activas.

    Los 10 mejores kits gratuitos de Tailwind CSS

    1. Flowbite

    • 400+ componentes y elementos interactivos.

    • Copiar y pegar directamente en tu proyecto.

    • Compatible con Laravel y Vue.js (para React usar Flowbite React).

    Ejemplo de botón Flowbite:

    <button type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Default</button>

    2. DaisyUI

    • Más de 50 componentes.

    • 15,000+ estrellas en GitHub.

    • Más de 2 millones de instalaciones en NPM.

    • Sintaxis limpia y temas personalizables.

    • Totalmente responsive.

    • Algunas críticas por colores predeterminados muy brillantes.

    3. Preline UI

    • Más de 300 componentes y 160 páginas de inicio.

    • Compatible con React, Next.js, Svelte, Remix, y más.

    • Incluye kit de Figma.

    4. HyperUI

    • Más de 42 componentes.

    • Enfocado en marketing, eCommerce y UIs de aplicaciones.

    • Ideal para prototipar rápidamente.

    5. TailGrids

    • 300+ componentes.

    • Compatible con React, Vue y Angular.

    • Documentación extensa con vistas en vivo.

    6. Headless UI

    • Componentes accesibles y sin estilos.

    • Diseñado para control total del estilo.

    • Integración perfecta con Tailwind CSS.

    7. Tailblocks

    • Componentes prediseñados para montar páginas rápido.

    • Diseño limpio y moderno.

    • Personalización limitada.

    8. Meraki UI

    • 108 componentes.

    • Soporta RTL y modo oscuro.

    • Uso de Flexbox y CSS Grid.

    9. Mamba UI

    • Más de 150 componentes (algunos gratuitos, otros pagos).

    • Código en HTML o JSX.

    • Diseños modernos y atractivos.

    10. Tailwind Starter Kit

    • Plantillas básicas de HTML.

    • Componentes dinámicos para React, Vue y Angular.

    • Código abierto y gratuito.

    Comparativa final

     

    Librería Componentes Estrellas GitHub Frameworks Soportados Personalización
    Flowbite 400+ 1,076 Alpine.js, Laravel, Vue.js Alta
    DaisyUI 56 28,494 Cualquiera compatible Alta
    Preline UI 300+ 3,200 React, Next.js, Svelte, Remix Alta
    HyperUI 42+ 6,949 Cualquiera compatible Moderada
    TailGrids 300+ N/D React, Vue, Angular Alta
    Headless UI Sin estilo 17,300 React, Vue Alta
    Tailblocks Variado N/D Cualquiera compatible Limitada
    Meraki UI 108 N/D Cualquiera compatible Alta
    Mamba UI 150+ 300 Angular, React, Svelte, Vue Moderada
    Tailwind Starter Kit 120+ 5,000 React, Vue, Angular Alta

    ¿Qué significa esto para ti?

    • ¿Necesitas muchos componentes? → Flowbite o Preline UI.

    • ¿Quieres apoyo de comunidad? → DaisyUI.

    • ¿Buscas accesibilidad en React/Vue? → Headless UI.

    • ¿Máxima personalización? → Casi todos ofrecen alta, excepto Tailblocks.

    Extras:

    • Meraki UI: Soporte RTL y modo oscuro.

    • Headless UI: Accesibilidad enfocada.

    • HyperUI: Especial para marketing y comercio electrónico.

    Cómo elegir:

    • ¿Cuántos componentes necesitas?

    • ¿Qué framework estás usando?

    • ¿Cuánto nivel de personalización deseas?

    • ¿Requieres características especiales (accesibilidad, RTL)?

    ✅ ¿Accesibilidad para React? → Headless UI
    ✅ ¿Muchos componentes pre-estilizados? → Flowbite o Preline UI

    Cierre

    Elegir el kit de UI adecuado en Tailwind CSS puede definir el éxito de tu proyecto. Considera:

    – Cantidad de componentes:

    • Flowbite: 400+

    • Tailwind Elements: 500+

    • TailGrids: 300+

    Para proyectos pequeños, DaisyUI o HyperUI son ideales.

    – Compatibilidad con frameworks:

    • Headless UI: React, Vue

    • Flowbite: Alpine.js, Laravel, Vue.js

    • TailGrids: React, Vue, Angular

    – Personalización:

    • Casi todas ofrecen alta personalización, excepto Tailblocks.

    – Características únicas:

    • Meraki UI: Idiomas RTL y modo oscuro

    • Headless UI: Accesibilidad

    • HyperUI: Componentes de marketing/eCommerce

  • ¡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).