Skip to content

Instantly share code, notes, and snippets.

@wolftrax5
Created January 19, 2026 20:34
Show Gist options
  • Select an option

  • Save wolftrax5/4b1e1b7a85840de036d20d499d43c736 to your computer and use it in GitHub Desktop.

Select an option

Save wolftrax5/4b1e1b7a85840de036d20d499d43c736 to your computer and use it in GitHub Desktop.
Hooks used on React
import { useEffect, RefObject } from 'react'
/**
* Hook que detecta clicks fuera de uno o múltiples elementos
*
* @param ref - Ref del elemento o array de refs de elementos a observar
* @param handler - Callback que se ejecuta cuando se hace click fuera del elemento
*/
export function useClickOutside(
ref: RefObject<HTMLElement> | RefObject<HTMLElement>[],
handler: (event: MouseEvent | TouchEvent) => void
): void {
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
// Convertir ref a array si es un solo ref
const refs = Array.isArray(ref) ? ref : [ref]
// Verificar si el click fue dentro de alguno de los elementos
const isClickInside = refs.some(refItem => {
// Verificar que el ref tenga un elemento actual
if (!refItem.current) {
return false
}
// Verificar si el target del evento está dentro del elemento
return refItem.current.contains(event.target as Node)
})
// Si el click fue fuera de todos los elementos, ejecutar el handler
if (!isClickInside) {
handler(event)
}
}
// Agregar event listeners para mouse y touch events
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('touchstart', handleClickOutside)
// Cleanup: remover event listeners cuando el componente se desmonte
// o cuando cambien las dependencias
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('touchstart', handleClickOutside)
}
}, [ref, handler]) // Re-ejecutar si ref o handler cambian
}
import { useState, useEffect } from 'react'
/**
* Hook que retorna un valor debounced después de un delay especificado
*
* @template T - Tipo genérico del valor a debounce
* @param value - El valor que se quiere debounce
* @param delay - El delay en milisegundos antes de actualizar el valor debounced
* @returns El valor debounced
*/
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
// Crear un timeout que actualizará el valor después del delay
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cleanup: limpiar el timeout si el valor o delay cambian antes de que se complete
// o si el componente se desmonta
return () => {
clearTimeout(handler)
}
}, [value, delay]) // Re-ejecutar cuando value o delay cambien
return debouncedValue
}
/**
* Hook para hacer peticiones HTTP
*
* Este hook:
* - Hace peticiones HTTP
* - Es genérico y tipado
* - Maneja estados: loading, data, error
* - Soporta refetch manual
* - Cancela peticiones (AbortController)
* - Cache opcional
* - Retry automático con backoff exponencial
*/
import { useState, useEffect, useRef, useCallback } from 'react'
interface UseFetchOptions {
enabled?: boolean
cache?: boolean
retry?: number
retryDelay?: number
}
interface UseFetchReturn<T> {
data: T | null
loading: boolean
error: Error | null
refetch: () => void
}
const cache = new Map<string, { data: unknown; timestamp: number }>()
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutos
export function useFetch<T>(
url: string | null,
options?: UseFetchOptions
): UseFetchReturn<T> {
const {
enabled = true,
cache: useCache = false,
retry = 0,
retryDelay = 1000
} = options || {}
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<Error | null>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const retryCountRef = useRef<number>(0)
const fetchData = useCallback(async () => {
if (!url || !enabled) {
return
}
// Verificar cache
if (useCache && cache.has(url)) {
const cached = cache.get(url)!
if (Date.now() - cached.timestamp < CACHE_DURATION) {
setData(cached.data as T)
setLoading(false)
setError(null)
return
}
}
// Cancelar petición anterior si existe
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
// Crear nuevo AbortController
abortControllerRef.current = new AbortController()
const signal = abortControllerRef.current.signal
setLoading(true)
setError(null)
const attemptFetch = async (attempt: number): Promise<void> => {
try {
const response = await fetch(url, { signal })
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const jsonData = await response.json()
// Guardar en cache si está habilitado
if (useCache) {
cache.set(url, { data: jsonData, timestamp: Date.now() })
}
setData(jsonData as T)
setLoading(false)
setError(null)
retryCountRef.current = 0
} catch (err: unknown) {
// No hacer retry si fue cancelado
if (err instanceof Error && err.name === 'AbortError') {
return
}
// Intentar retry si quedan intentos
if (attempt < retry) {
const delay = retryDelay * Math.pow(2, attempt) // Backoff exponencial
await new Promise(resolve => setTimeout(resolve, delay))
return attemptFetch(attempt + 1)
}
setError(err instanceof Error ? err : new Error(String(err)))
setLoading(false)
retryCountRef.current = 0
}
}
await attemptFetch(0)
}, [url, enabled, useCache, retry, retryDelay])
useEffect(() => {
fetchData()
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
}
}, [fetchData])
const refetch = useCallback(() => {
retryCountRef.current = 0
fetchData()
}, [fetchData])
return {
data,
loading,
error,
refetch
}
}
/**
* Hook que observa la intersección de elementos
*
* Este hook:
* - Observa intersección de elementos
* - Configuración de threshold y rootMargin
* - Retorna si el elemento está visible
* - Cleanup automático del observer
*/
import { useState, useEffect, RefObject } from 'react'
interface UseIntersectionObserverOptions {
threshold?: number | number[]
rootMargin?: string
root?: Element | null
}
export function useIntersectionObserver(
elementRef: RefObject<Element>,
options?: UseIntersectionObserverOptions
): boolean {
const [isIntersecting, setIsIntersecting] = useState<boolean>(false)
useEffect(() => {
const element = elementRef.current
if (!element) {
return
}
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting)
},
{
threshold: options?.threshold ?? 0,
rootMargin: options?.rootMargin ?? '0px',
root: options?.root ?? null
}
)
observer.observe(element)
return () => {
observer.disconnect()
}
}, [elementRef, options?.threshold, options?.rootMargin, options?.root])
return isIntersecting
}
/**
* Hook que sincroniza estado con localStorage
*
* Este hook:
* - Sincroniza estado con localStorage
* - Es genérico y tipado
* - Maneja serialización/deserialización automática
* - Maneja errores de localStorage
* - Sincroniza entre múltiples tabs/windows
* - Retorna [value, setValue, removeValue]
*/
import { useState, useEffect, useCallback } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((val: T) => T)) => void, () => void] {
// Función para leer del localStorage
const readValue = useCallback((): T => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error)
return initialValue
}
}, [initialValue, key])
// Estado que se sincroniza con localStorage
const [storedValue, setStoredValue] = useState<T>(readValue)
// Función para actualizar el valor
const setValue = useCallback(
(value: T | ((val: T) => T)) => {
try {
// Permitir que value sea una función para actualizar basado en el valor anterior
const valueToStore = value instanceof Function ? value(storedValue) : value
// Guardar estado
setStoredValue(valueToStore)
// Guardar en localStorage
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
// Disparar evento personalizado para sincronizar entre tabs
window.dispatchEvent(new Event('local-storage'))
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error)
}
},
[key, storedValue]
)
// Función para remover el valor
const removeValue = useCallback(() => {
try {
setStoredValue(initialValue)
if (typeof window !== 'undefined') {
window.localStorage.removeItem(key)
window.dispatchEvent(new Event('local-storage'))
}
} catch (error) {
console.warn(`Error removing localStorage key "${key}":`, error)
}
}, [key, initialValue])
// Efecto para sincronizar entre tabs/windows
useEffect(() => {
setStoredValue(readValue())
const handleStorageChange = () => {
setStoredValue(readValue())
}
// Escuchar cambios en localStorage de otras tabs/windows
window.addEventListener('storage', handleStorageChange)
// Escuchar eventos personalizados del mismo tab
window.addEventListener('local-storage', handleStorageChange)
return () => {
window.removeEventListener('storage', handleStorageChange)
window.removeEventListener('local-storage', handleStorageChange)
}
}, [readValue])
return [storedValue, setValue, removeValue]
}
/**
* Hook que obtiene el valor anterior de un estado
*
* Este hook:
* - Obtiene el valor anterior de un estado
* - Es genérico y tipado
* - Retorna undefined en el primer render
*/
import { useRef, useEffect } from 'react'
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
/**
* Hook que alterna entre true/false
*
* Este hook:
* - Alterna entre true/false
* - Retorna [value, toggle, setValue]
*/
import { useState, useCallback } from 'react'
export function useToggle(
initialValue: boolean = false
): [boolean, () => void, (value: boolean) => void] {
const [value, setValue] = useState<boolean>(initialValue)
const toggle = useCallback(() => {
setValue(prev => !prev)
}, [])
const setValueDirect = useCallback((newValue: boolean) => {
setValue(newValue)
}, [])
return [value, toggle, setValueDirect]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment