From n00b to ZeroCool / Origen

DOM: manipular la realidad desde el navegador (sin romper todo)

Domina el DOM con ejemplos reales en React + Vite + Tailwind: selectores, eventos, UI updates, DevTools y errores típicos del jale.

Lo que vale la pena leer aquí

Silencio incómodo.

Intro con gancho

Estás en una junta por Meet. Tu compa de ventas comparte pantalla y hace clic en “Pagar”. No pasa nada.

Silencio incómodo.

Tú con una laptop ya guerrera (ventilador en modo helicóptero), el jefe en el chat: “¿lo puedes sacar en corto? es para production”, y el deadline respirándote en la nuca.

Abres DevTools. El botón sí existe… pero está disabled por una clase, o el click se lo está tragando un overlay, o el handler nunca se conectó. Eso que estás tocando —clases, atributos, texto, eventos— es el DOM: la “realidad” que el navegador está renderizando.

Cuando entiendes el DOM, dejas de adivinar. Empiezas a mover la UI con intención y con menos rollback.

Qué vas a aprender

  • Qué es el DOM en la práctica (sin definición de Wikipedia), y cómo se amarra con HTML/CSS/JS.
  • Cómo seleccionar elementos y leer/modificar texto, atributos y clases.
  • Cómo escuchar eventos sin armar un desastre (click, input, submit).
  • Cómo hacerlo “a mano” en JS y cómo se traduce a React con Vite + Tailwind.
  • Un workflow real: reproducir bug, inspeccionar, parchear, y no romper accesibilidad.

Contexto práctico

Piensa en el DOM como el árbol de nodos que el navegador construye a partir de tu HTML. Cada etiqueta se vuelve un nodo; los atributos y el texto viven ahí. CSS decide cómo se ve; JS puede leer y editar esa estructura.

Cosas bien de jale que dependen del DOM:

  • Validación en vivo de un form (cuando el cliente te dice “que avise desde que escribo”).
  • Abrir/cerrar un modal sin recargar página.
  • Cambiar estado visual: loading spinners, disabled, error, success.
  • Trackear analytics cuando alguien hace click… y luego descubrir que ese click nunca pasó.

Y sí: en React “no deberías” estar manoseando el DOM directo para todo… pero igual lo necesitas para focus, scroll, medir tamaños, integrar librerías y debuggear cuando el stack se pone punk.

Paso a paso o guía principal

1) Monta un mini-lab con Vite + React + Tailwind

Si ya tienes proyecto, salta al componente. Si no, arranca uno limpio para experimentar sin miedo (y sin romper el repo del equipo).

npm create vite@latest dom-lab -- --template react
cd dom-lab
npm i
npm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

En tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

En src/index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

Corre:

npm run dev

2) Domina el DOM “vanilla”: seleccionar, leer y modificar

Antes de React, un ejemplo honesto en JS puro. Esto te aterriza qué está pasando debajo del framework.

Supón este HTML:

<div id="app">
  <h1 class="title">Hola</h1>
  <button id="btn" class="btn">Cambiar</button>
</div>

Selección (lo típico y lo peligroso):

const title = document.querySelector('.title')
const btn = document.getElementById('btn')

btn.addEventListener('click', () => {
  title.textContent = 'Hola, DOM'
  title.classList.toggle('text-green-600')
})

Decisión práctica:

  • getElementById es rápido y claro cuando tienes un id único.
  • querySelector es flexible, pero si tu selector es ambiguo (o cambia por Tailwind/markup/refs), te puedes disparar en el pie.

3) Eventos: donde nacen bugs raros

Ejemplo: un formulario.

<form id="signup">
  <input id="email" type="email" />
  <button>Enviar</button>
</form>
const form = document.getElementById('signup')
const email = document.getElementById('email')

form.addEventListener('submit', (e) => {
  e.preventDefault()

  if (!email.value.includes('@')) {
    email.setAttribute('aria-invalid', 'true')
    email.classList.add('border-red-500')
    return
  }

  // Simula envío
  console.log('OK:', email.value)
})

Decisión práctica:

  • preventDefault() es tu compa cuando no quieres recargar la página.
  • Si marcas un input como inválido, no todo es “pintarlo rojo”: usa aria-invalid para accesibilidad. En muchos equipos eso se ignora… hasta que cae auditoría o llega cliente enterprise.

4) El DOM en React: tu UI es una función (pero el DOM sigue ahí)

React genera/actualiza el DOM por ti. Regla general: no manipules el DOM directo para cosas de estado, usa useState.

Pero hay casos donde sí aplica tocar el DOM: focus, scroll, medir.

Vamos a armar un componente realista: lista de tareas mini con input, validación y scroll al último item. Nada de “contador” de tutorial.

Crea src/DomReality.jsx:

import { useEffect, useMemo, useRef, useState } from 'react'

export default function DomReality() {
  const [text, setText] = useState('')
  const [items, setItems] = useState([])
  const [error, setError] = useState('')
  const inputRef = useRef(null)
  const lastItemRef = useRef(null)

  const remaining = useMemo(() => 50 - text.length, [text])

  useEffect(() => {
    // UX real: al cargar, el focus te ahorra un click.
    inputRef.current?.focus()
  }, [])

  useEffect(() => {
    // DOM “permitido”: scroll al último elemento cuando cambia la lista.
    lastItemRef.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
  }, [items.length])

  function addItem(e) {
    e.preventDefault()

    const trimmed = text.trim()
    if (!trimmed) {
      setError('Escribe algo antes de agregar.')
      inputRef.current?.focus()
      return
    }

    if (trimmed.length > 50) {
      setError('Te pasaste de 50 caracteres. Baja tantito.')
      inputRef.current?.focus()
      return
    }

    setItems(prev => [...prev, { id: crypto.randomUUID(), label: trimmed }])
    setText('')
    setError('')
  }

  return (
    <div className="min-h-screen bg-slate-950 text-slate-100 p-6">
      <div className="max-w-xl mx-auto">
        <h1 className="text-2xl font-semibold">DOM: manipular la realidad</h1>
        <p className="text-slate-300 mt-2">
          React actualiza el DOM por ti, pero tú decides el comportamiento.
        </p>

        <form onSubmit={addItem} className="mt-6 space-y-3">
          <label className="block">
            <span className="text-sm text-slate-300">Nueva tarea</span>
            <input
              ref={inputRef}
              value={text}
              onChange={(e) => setText(e.target.value)}
              aria-invalid={error ? 'true' : 'false'}
              className={
                "mt-1 w-full rounded-lg bg-slate-900 border px-3 py-2 outline-none " +
                (error ? "border-red-500" : "border-slate-700 focus:border-indigo-500")
              }
              placeholder="Ej: pagar el dominio antes de que lo suba a $900"
            />
          </label>

          <div className="flex items-center justify-between text-xs text-slate-400">
            <span>{remaining} caracteres</span>
            <span className={remaining < 0 ? 'text-red-400' : ''}>
              {remaining < 0 ? 'Te pasaste' : 'Vas bien'}
            </span>
          </div>

          {error && (
            <div className="rounded-md border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-200">
              {error}
            </div>
          )}

          <button
            type="submit"
            className="rounded-lg bg-indigo-600 hover:bg-indigo-500 px-4 py-2 font-medium"
          >
            Agregar
          </button>
        </form>

        <div className="mt-8">
          <h2 className="text-lg font-semibold">Lista</h2>
          {items.length === 0 ? (
            <p className="text-slate-400 mt-2">Vacía. Como el refri el día 28.</p>
          ) : (
            <ul className="mt-3 space-y-2">
              {items.map((it, idx) => {
                const isLast = idx === items.length - 1
                return (
                  <li
                    key={it.id}
                    ref={isLast ? lastItemRef : null}
                    className="rounded-lg bg-slate-900 border border-slate-800 px-3 py-2"
                  >
                    {it.label}
                  </li>
                )
              })}
            </ul>
          )}
        </div>
      </div>
    </div>
  )
}

Y úsalo en src/App.jsx:

import DomReality from './DomReality'

export default function App() {
  return <DomReality />
}

Decisiones prácticas (las que sí te pegan en la talacha):

  • useState controla lo que se ve. Si intentas “setear” el DOM manualmente y React re-renderiza, tu cambio desaparece como si nada.
  • useRef es para tocar DOM de forma puntual: focus, scroll, medir. No para guardar “estado” de negocio.
  • useEffect sincroniza con el DOM después de render. Si lo haces antes, ref.current puede ser null y te comes un error justo cuando estás compartiendo pantalla.
DOM: manipular la realidad desde el navegador (sin romper todo) - visual explicativa 1
Visual de apoyo: Intro con gancho

5) Debugging con DevTools: cuando la realidad no coincide con tu cabeza

Bug clásico: “sí se actualiza el state, pero la UI no cambia”.

Checklist mental rápido:

  • ¿El componente se re-renderiza? Pon un console.log('render') o usa React DevTools.
  • ¿El elemento existe? Inspecciona el nodo en Elements.
  • ¿Una clase te está ganando? En Tailwind, a veces el orden de clases o estados (hover:, focus:) te engaña. Checa el panel de “Computed”.
  • ¿Evento mal conectado? En React: onClick={fn} no onClick={fn()} (ese lo ejecuta al render).

Escena muy real: el wifi del cowork se pone raro, recargas, HMR se queda colgado, y juras que tu cambio “no pegó”. Antes de culpar al DOM: reinicia el dev server y revisa la consola de Vite.

A mí una vez me pasó que el navegador se quedó con un service worker de un PWA viejo y yo estaba viendo una UI que no era el build actual. Dos horas tiradas por no revisar Application > Service Workers.

Screenshots sugeridos

  1. DevTools en la pestaña Elements mostrando el input con aria-invalid="true" y la clase border-red-500.
  2. Pestaña Console mostrando logs de render o un error típico (Cannot read properties of null (reading 'focus')).
  3. React DevTools mostrando el state items creciendo y el text reiniciándose.
  4. Pestaña Computed enseñando qué regla CSS está dominando cuando “no se pinta” el borde.

Errores comunes + solución

Error 1: querySelector regresa null

Síntoma: Cannot read properties of null.

Causa real: el script corre antes de que el DOM exista, o el selector no coincide.

Arreglo:

  • En vanilla: mueve tu script al final del body o usa DOMContentLoaded.
document.addEventListener('DOMContentLoaded', () => {
  const el = document.querySelector('#btn')
  // ...
})
  • En React: usa ref + useEffect, no document.querySelector.

Error 2: “Le pongo una clase y no se ve” (Tailwind edition)

Síntoma: agregas border-red-500 y nada.

Causas comunes:

  • Tienes otra clase que pisa (ej. border-slate-700 después).
  • Te falta border (en Tailwind, el color sin ancho/estilo a veces no hace lo que esperas).

Arreglo:

  • Asegura que exista border y ordena clases conscientemente.
  • Confirma en DevTools qué regla gana.

Error 3: Ejecutar handlers en render

Síntoma: algo se dispara “solo” al cargar.

Causa: onClick={miFuncion()}.

Arreglo: onClick={miFuncion} o onClick={() => miFuncion(arg)}.

Error 4: Manipular DOM directo y React “deshace” tu cambio

Síntoma: cambias textContent o classList y luego vuelve a lo anterior.

Causa: React re-renderiza y reconstruye el DOM según state/props.

Arreglo: si es visual/estado, muévelo a useState. Si es focus/scroll/medición, useRef + useEffect.

Error 5: useEffect en loop infinito

Síntoma: la app se traba y la consola se llena.

Causa: dependencias mal puestas (useEffect(() => setState(...), [state])).

Arreglo:

  • Revisa dependencias.
  • Si necesitas derivar valores, usa useMemo o calcula inline.
DOM: manipular la realidad desde el navegador (sin romper todo) - visual explicativa 2
Visual de apoyo: Qué vas a aprender

Checklist final

  • Puedo explicar qué parte es DOM (estructura), qué parte es CSS (apariencia) y qué parte es JS/React (comportamiento).
  • Sé seleccionar elementos en vanilla (getElementById, querySelector) y entiendo por qué puede regresar null.
  • Sé escuchar eventos (click, input, submit) sin recargar la página cuando no quiero.
  • En React, uso useState para UI y useRef para tocar el DOM (focus/scroll/measure).
  • Puedo inspeccionar un nodo en DevTools y detectar si una clase está siendo pisada.
  • Si algo “no cambia”, sé verificar render/state con React DevTools.

FAQ

1) ¿El DOM es lo mismo que HTML?

No. HTML es el texto fuente. El DOM es el árbol de nodos que el navegador construye a partir del HTML (y que JS puede modificar).

2) ¿Por qué en React “no debo” usar document.querySelector?

Porque React administra el DOM. Si tú lo manipulas por fuera para cosas de estado visual, se va a desincronizar. Para casos puntuales, usa ref.

3) ¿Cuándo sí conviene usar useRef?

Focus de inputs, scroll a un elemento, medir tamaños/posiciones, integrar una librería que necesita un nodo real (charts, maps, etc.).

4) ¿Qué hago si Tailwind no aplica mis estilos?

Revisa orden de clases, confirma que el archivo está incluido en content de tailwind.config.js, y valida en DevTools qué regla CSS está ganando.

5) ¿Cómo debuggeo eventos que “no se disparan”?

En DevTools inspecciona el elemento, revisa si hay overlays (un div encima), valida que el handler esté conectado (en React, que no estés llamando la función en render), y prueba con un console.log dentro del handler.

Siguiente episodio: teaser

Ya que puedes manipular la realidad, toca organizarla: estado, componentes y flujo.
Vamos a llevar este caos bonito a algo mantenible sin perder velocidad de entrega.