useRef: referencias y una caja arrastrable
useRef devuelve un objeto estable con una propiedad current. Su valor se conserva entre renders, pero cambiarlo no solicita un render nuevo. Es útil para guardar un nodo del DOM o un dato auxiliar que no debe mostrarse directamente.
Referencia a un elemento #
1import { useRef } from 'react'; 2 3export default function EnfocarCampo() { 4 const inputRef = useRef<HTMLInputElement>(null); 5 return ( 6 <> 7 <input ref={inputRef} aria-label="Nombre" /> 8 <button onClick={() => inputRef.current?.focus()}>Enfocar</button> 9 </> 10 ); 11}
React coloca el nodo del input en current cuando lo conecta al DOM. Antes puede ser null; por eso comprobamos su existencia con ?..
Práctica: una caja arrastrable #
Ahora sustituye el ejemplo anterior por este contenido en App.tsx. Usaremos estado para la posición visible y una referencia para recordar por dónde cogimos la caja. Los eventos de puntero funcionan con ratón, lápiz y pantalla táctil.
1import { useRef, useState, type PointerEvent } from 'react'; 2 3function DraggableBox({ initialY = 0 }: { initialY?: number }) { 4 const [position, setPosition] = useState({ x: 0, y: initialY }); 5 const [dragging, setDragging] = useState(false); 6 const offsetRef = useRef({ x: 0, y: 0 }); 7 const pointerRef = useRef<number | null>(null); 8 9 function handlePointerDown(event: PointerEvent<HTMLDivElement>) { 10 if (event.button !== 0 || pointerRef.current !== null) return; 11 const box = event.currentTarget.getBoundingClientRect(); 12 offsetRef.current = { 13 x: event.clientX - box.left, 14 y: event.clientY - box.top, 15 }; 16 pointerRef.current = event.pointerId; 17 event.currentTarget.setPointerCapture(event.pointerId); 18 setDragging(true); 19 } 20 21 function handlePointerMove(event: PointerEvent<HTMLDivElement>) { 22 if (pointerRef.current !== event.pointerId) return; 23 const box = event.currentTarget; 24 const parent = box.parentElement; 25 if (!parent) return; 26 const bounds = parent.getBoundingClientRect(); 27 const maxX = Math.max(0, parent.clientWidth - box.offsetWidth); 28 const maxY = Math.max(0, parent.clientHeight - box.offsetHeight); 29 const x = event.clientX - bounds.left - parent.clientLeft - offsetRef.current.x; 30 const y = event.clientY - bounds.top - parent.clientTop - offsetRef.current.y; 31 32 setPosition({ 33 x: Math.max(0, Math.min(x, maxX)), 34 y: Math.max(0, Math.min(y, maxY)), 35 }); 36 } 37 38 function stopDragging(event: PointerEvent<HTMLDivElement>) { 39 if (pointerRef.current !== event.pointerId) return; 40 pointerRef.current = null; 41 setDragging(false); 42 } 43 44 return ( 45 <div 46 onPointerDown={handlePointerDown} 47 onPointerMove={handlePointerMove} 48 onPointerUp={stopDragging} 49 onPointerCancel={stopDragging} 50 onLostPointerCapture={stopDragging} 51 style={{ 52 position: 'absolute', 53 left: position.x, 54 top: position.y, 55 width: 100, 56 height: 100, 57 display: 'grid', 58 placeItems: 'center', 59 backgroundColor: dragging ? '#c88310' : '#FAC898', 60 color: 'black', 61 userSelect: 'none', 62 touchAction: 'none', 63 cursor: dragging ? 'grabbing' : 'grab', 64 borderRadius: 8, 65 }} 66 > 67 Arrástrame 68 </div> 69 ); 70} 71 72export default function Ejem_24() { 73 return ( 74 <div 75 style={{ 76 position: 'relative', 77 width: '100%', 78 minWidth: 100, 79 height: 340, 80 backgroundColor: '#8c8c8c', 81 overflow: 'hidden', 82 }} 83 > 84 <DraggableBox /> 85 <DraggableBox initialY={110} /> 86 <DraggableBox initialY={220} /> 87 </div> 88 ); 89}
Qué guarda cada dato #
position y dragging son estados porque determinan el aspecto de la caja. offsetRef guarda la distancia entre el puntero y la esquina de la caja al comenzar. Si no restáramos esa distancia, la caja saltaría para colocar su esquina bajo el puntero.
pointerRef identifica el puntero activo y se actualiza inmediatamente sin necesitar un render. Cada caja tiene sus propias referencias y estado.
getBoundingClientRect() mide coordenadas respecto de la ventana; clientX y clientY usan ese mismo sistema. Restamos el origen del padre para obtener una posición dentro del contenedor.
Math.min y Math.max limitan la posición entre cero y el espacio disponible, descontando el tamaño de la caja. overflow: hidden solo ocultaría lo que saliera; por sí mismo no limita las coordenadas.
Captura y final de la interacción #
setPointerCapture permite que la caja siga recibiendo movimientos y la finalización aunque el puntero salga de ella. El navegador libera normalmente la captura al terminar o cancelar la interacción. También restauramos el estado cuando se pierde la captura. No registramos listeners globales en document, por lo que no hay listeners manuales que retirar al desmontar.
Esta práctica enseña el mecanismo de arrastre. Para una interfaz que necesite mover elementos también con teclado, añade controles equivalentes, como botones de dirección, con límites iguales.
Prueba tú #
Arrastra cada caja desde distintos puntos y comprueba que no salta al comenzar, no sale del padre y vuelve a su color normal al soltar. Cambia el ancho de la ventana y vuelve a moverla para comprobar el nuevo límite.
Consulta la referencia oficial de useRef.
Cuestionario del tema #
Abre el cuestionario en las actividades de este tema para comprobar lo aprendido. Cada respuesta incluye una explicación.