Ver los cambios realizados en el Hook CHANGELOG
- Conoce las mejoras y cambios en la funcionalidad del Hook FUNCTIONALITY
Hook de React para gestión avanzada de formularios con validación, seguridad y TypeScript
- Instalación
- Inicio Rápido
- Características
- Modos de Uso
- Ejemplos
- Validación con Zod
- API Reference
- Seguridad
- Mejores Prácticas
npm install usetargethandlernpm update usetargethandlerimport { useTargetHandler } from "usetargethandler";import { useTargetHandler, ValidationRules, FormValues } from "usetargethandler";import { useTargetHandler, z } from "usetargethandler";import { useTargetHandler } from "usetargethandler";
function LoginForm() {
const [target, handleTarget, handleSubmit, errors] = useTargetHandler(
{ email: "", password: "" },
{
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
patternMessage: "Email inválido"
},
password: {
required: true,
minLength: 6,
minLengthMessage: "Mínimo 6 caracteres"
}
}
);
const onSubmit = (data) => {
console.log("Formulario válido:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="email"
name="email"
value={target.email}
onChange={handleTarget}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email.message}</span>}
<input
type="password"
name="password"
value={target.password}
onChange={handleTarget}
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password.message}</span>}
<button type="submit">Iniciar Sesión</button>
</form>
);
}- Soporte TypeScript completo (v1.4.0+) - Tipos completos, funciona sin configuración adicional
- Zod integrado (v1.4.0+) - Validación avanzada sin instalación adicional
- Validaciones personalizadas - Sistema de reglas flexible y extensible
- Detección automática - El hook detecta si usas Zod o ValidationRules nativas
- Protección XSS - Sanitización automática con DOMPurify
- Protección CSRF - Token automático en solicitudes HTTP
- Rate Limiting - Prevención de spam y ataques
- Validación de entrada - Filtrado de datos maliciosos
- Gestión de estado en tiempo real - Actualización instantánea de campos
- Persistencia de datos - LocalStorage o SessionStorage configurable
- Estados de carga - Indicadores
isLoadingintegrados - Reinicialización automática - Limpieza del formulario tras envío
- useHttpRequest integrado - Llamadas API simplificadas
- Sentry opcional - Monitoreo de errores y eventos
- Variables de entorno - Configuración flexible de API URL
- Respuestas tipadas - Manejo de respuestas con TypeScript
Perfecto para formularios simples sin llamadas API.
import { useTargetHandler } from "usetargethandler";
const [target, handleTarget, handleSubmit, errors] = useTargetHandler(
{ email: "", password: "" },
{
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
password: { required: true, minLength: 6 }
},
{ storageType: "session", storageKey: "loginForm" },
{ enableCSRF: false, rateLimit: 1000 }
);Para formularios que necesitan comunicarse con APIs.
import { useTargetHandler } from "usetargethandler";
import { useHttpRequest } from "usehttprequest";
const [target, handleTarget, handleSubmit, errors, httpRequest] = useTargetHandler(
{ email: "", password: "" },
{
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
password: { required: true, minLength: 6 }
},
{ storageType: "session", storageKey: "loginForm" },
{ enableCSRF: true, rateLimit: 3000 },
useHttpRequest // ← Habilita funcionalidades HTTP
);
// Acceso a funcionalidades HTTP
const {
apiCall, // Llamadas a la API
apiResponse, // Respuesta de la última llamada
isLoading, // Estado de carga
error, // Errores HTTP
SentryError, // Logging de errores
SentryInfo // Logging de información
} = httpRequest;# Vite
VITE_API_URL=https://api.ejemplo.com
# Create React App
REACT_APP_API_URL=https://api.ejemplo.com
⚠️ Importante: Reinicia el servidor de desarrollo después de modificar.env
import React from "react";
import { useTargetHandler } from "usetargethandler";
import { useHttpRequest } from "usehttprequest";
export function LoginForm() {
const [target, handleTarget, handleSubmit, errors, httpRequest] = useTargetHandler(
{ email: "", password: "" },
{
email: {
required: true,
requiredMessage: "Email es requerido",
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
patternMessage: "Email inválido",
},
password: {
required: true,
minLength: 6,
minLengthMessage: "Mínimo 6 caracteres",
},
},
{ storageType: "session", storageKey: "loginForm" },
{ enableCSRF: true, rateLimit: 3000 },
useHttpRequest
);
const { apiCall, isLoading, error, apiResponse, SentryError, SentryInfo } = httpRequest;
const onSubmit = async (data: typeof initialValues) => {
try {
SentryInfo("Intentando login", { email: data.email });
await apiCall("POST", "/auth/login", data);
if (apiResponse) {
localStorage.setItem("token", apiResponse.token);
window.location.href = "/dashboard";
}
if (error) {
SentryError("Error en login", error);
}
} catch (err) {
console.error("Error:", err);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Email</label>
<input
type="email"
name="email"
value={target.email}
onChange={handleTarget}
/>
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<label>Password</label>
<input
type="password"
name="password"
value={target.password}
onChange={handleTarget}
/>
{errors.password && <span className="error">{errors.password.message}</span>}
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? "Iniciando sesión..." : "Iniciar Sesión"}
</button>
{error && <div className="error">Error: {error.message}</div>}
</form>
);
}import { useTargetHandler } from "usetargethandler";
export function RegisterForm() {
const [target, handleTarget, handleSubmit, errors] = useTargetHandler(
{
nombre: "",
email: "",
password: "",
confirmPassword: "",
age: "",
terms: false,
ciudad: "",
},
{
nombre: {
required: true,
requiredMessage: "El nombre es obligatorio",
pattern: /^[a-zA-Z\s]+$/,
patternMessage: "Solo letras permitidas",
minLength: 2,
maxLength: 50,
},
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
patternMessage: "Email inválido",
},
password: {
required: true,
minLength: 8,
minLengthMessage: "Mínimo 8 caracteres",
},
confirmPassword: {
required: true,
matches: "password",
matchMessage: "Las contraseñas no coinciden",
},
age: {
required: true,
min: 18,
max: 99,
requiredMessage: "Debes ser mayor de 18 años",
},
terms: {
checked: true,
checkedMessage: "Debes aceptar los términos",
},
ciudad: {
selected: true,
selectedMessage: "Selecciona una ciudad",
},
},
{ storageType: "local", storageKey: "registerForm" },
{ enableCSRF: true, rateLimit: 3000 }
);
const onSubmit = (data) => {
console.log("Registro exitoso:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="text"
name="nombre"
value={target.nombre}
onChange={handleTarget}
placeholder="Nombre completo"
/>
{errors.nombre && <span>{errors.nombre.message}</span>}
<input
type="email"
name="email"
value={target.email}
onChange={handleTarget}
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
<input
type="password"
name="password"
value={target.password}
onChange={handleTarget}
placeholder="Contraseña"
/>
{errors.password && <span>{errors.password.message}</span>}
<input
type="password"
name="confirmPassword"
value={target.confirmPassword}
onChange={handleTarget}
placeholder="Confirmar contraseña"
/>
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
<input
type="number"
name="age"
value={target.age}
onChange={handleTarget}
placeholder="Edad"
/>
{errors.age && <span>{errors.age.message}</span>}
<select name="ciudad" value={target.ciudad} onChange={handleTarget}>
<option value="">Selecciona una ciudad</option>
<option value="madrid">Madrid</option>
<option value="barcelona">Barcelona</option>
<option value="valencia">Valencia</option>
</select>
{errors.ciudad && <span>{errors.ciudad.message}</span>}
<label>
<input
type="checkbox"
name="terms"
checked={target.terms}
onChange={handleTarget}
/>
Acepto los términos y condiciones
</label>
{errors.terms && <span>{errors.terms.message}</span>}
<button type="submit">Registrarse</button>
</form>
);
}// Obtener lista de usuarios con paginación
const fetchUsers = async (page = 1, limit = 10) => {
await apiCall("GET", `/users?page=${page}&limit=${limit}`);
if (apiResponse) {
console.log("Usuarios:", apiResponse.data);
console.log("Total:", apiResponse.total);
}
};
// Buscar usuarios con filtros
const searchUsers = async (searchTerm: string) => {
await apiCall("GET", `/users/search?q=${encodeURIComponent(searchTerm)}&status=active`);
};
// Obtener detalle de un usuario específico
const getUserById = async (userId: string) => {
await apiCall("GET", `/users/${userId}`);
};// Crear nuevo usuario con validación
const createUser = async (formData: typeof target) => {
await apiCall("POST", "/users", {
name: formData.name,
email: formData.email,
role: "user",
metadata: {
createdAt: new Date().toISOString(),
source: "web"
}
});
if (apiResponse) {
console.log("Usuario creado:", apiResponse.id);
// Redirigir o mostrar mensaje de éxito
}
};
// Subir archivo con FormData
const uploadAvatar = async (file: File, userId: string) => {
const formData = new FormData();
formData.append("avatar", file);
formData.append("userId", userId);
await apiCall("POST", "/upload/avatar", formData);
};
// Login con credenciales
const login = async () => {
await apiCall("POST", "/auth/login", {
email: target.email,
password: target.password,
rememberMe: true
});
if (apiResponse?.token) {
localStorage.setItem("authToken", apiResponse.token);
window.location.href = "/dashboard";
}
};// Actualizar perfil completo del usuario
const updateUserProfile = async (userId: string) => {
await apiCall("PUT", `/users/${userId}`, {
name: target.name,
email: target.email,
phone: target.phone,
address: {
street: target.street,
city: target.city,
country: target.country
},
preferences: {
language: "es",
notifications: true
}
});
};// Actualizar solo el email del usuario
const updateEmail = async (userId: string, newEmail: string) => {
await apiCall("PATCH", `/users/${userId}`, {
email: newEmail
});
};
// Cambiar estado de un pedido
const updateOrderStatus = async (orderId: string, status: string) => {
await apiCall("PATCH", `/orders/${orderId}`, {
status,
updatedAt: new Date().toISOString()
});
};
// Activar/desactivar usuario
const toggleUserStatus = async (userId: string, isActive: boolean) => {
await apiCall("PATCH", `/users/${userId}/status`, {
isActive
});
};// Eliminar usuario con confirmación
const deleteUser = async (userId: string) => {
if (confirm("¿Estás seguro de eliminar este usuario?")) {
await apiCall("DELETE", `/users/${userId}`);
if (!error) {
console.log("Usuario eliminado exitosamente");
// Actualizar lista o redirigir
}
}
};
// Eliminar múltiples elementos
const deleteBatch = async (userIds: string[]) => {
await apiCall("DELETE", "/users/batch", {
ids: userIds
});
};
// Soft delete (marcar como eliminado)
const softDeleteUser = async (userId: string) => {
await apiCall("PATCH", `/users/${userId}`, {
deletedAt: new Date().toISOString(),
isDeleted: true
});
};import { useState, useEffect } from "react";
import { useTargetHandler } from "usetargethandler";
import { useHttpRequest } from "usehttprequest";
export function UsersList() {
const [page, setPage] = useState(1);
const [users, setUsers] = useState([]);
const [totalPages, setTotalPages] = useState(0);
const [target, handleTarget, handleSubmit, errors, httpRequest] = useTargetHandler(
{ search: "" },
{},
{},
{ enableCSRF: true, rateLimit: 2000 },
useHttpRequest
);
const { apiCall, isLoading, apiResponse } = httpRequest;
// Cargar usuarios al montar o cambiar página
useEffect(() => {
fetchUsers(page);
}, [page]);
const fetchUsers = async (currentPage: number) => {
await apiCall("GET", `/users?page=${currentPage}&limit=20&sort=createdAt:desc`);
if (apiResponse) {
setUsers(apiResponse.data);
setTotalPages(Math.ceil(apiResponse.total / 20));
}
};
// Buscar usuarios con debounce
const handleSearch = async (searchTerm: string) => {
if (searchTerm.length > 2) {
await apiCall("GET", `/users/search?q=${encodeURIComponent(searchTerm)}`);
if (apiResponse) {
setUsers(apiResponse.data);
}
} else if (searchTerm.length === 0) {
fetchUsers(page);
}
};
return (
<div>
<input
type="text"
name="search"
value={target.search}
onChange={(e) => {
handleTarget(e);
handleSearch(e.target.value);
}}
placeholder="Buscar usuarios..."
/>
{isLoading ? (
<div>Cargando...</div>
) : (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
)}
<div className="pagination">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1 || isLoading}
>
Anterior
</button>
<span>Página {page} de {totalPages}</span>
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages || isLoading}
>
Siguiente
</button>
</div>
</div>
);
}Zod viene incluido en el paquete, sin instalación adicional.
import { useTargetHandler, z } from "usetargethandler";
// Define el schema de validación
const registroSchema = z
.object({
email: z
.string()
.email("Email inválido")
.transform((val) => val.toLowerCase()),
username: z
.string()
.min(3, "Mínimo 3 caracteres")
.max(20, "Máximo 20 caracteres")
.regex(/^[a-zA-Z0-9_]+$/, "Solo letras, números y guión bajo"),
password: z
.string()
.min(8, "Mínimo 8 caracteres")
.regex(/[A-Z]/, "Debe contener una mayúscula")
.regex(/[0-9]/, "Debe contener un número"),
confirmPassword: z.string(),
age: z.number().min(18, "Mayor de 18 años").max(99),
terms: z.boolean().refine((val) => val === true, {
message: "Debes aceptar los términos",
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Las contraseñas no coinciden",
path: ["confirmPassword"],
});
// TypeScript infiere tipos automáticamente
type RegistroForm = z.infer<typeof registroSchema>;
export function FormularioZod() {
const [target, handleTarget, handleSubmit, errors] =
useTargetHandler<RegistroForm>(
{
email: "",
username: "",
password: "",
confirmPassword: "",
age: 0,
terms: false,
},
registroSchema // ← Detección automática de Zod
);
const onSubmit = async (data: RegistroForm) => {
console.log("Datos validados:", data);
// data.email está en minúsculas automáticamente
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="email"
name="email"
value={target.email}
onChange={handleTarget}
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
<input
type="text"
name="username"
value={target.username}
onChange={handleTarget}
placeholder="Usuario"
/>
{errors.username && <span>{errors.username.message}</span>}
<input
type="password"
name="password"
value={target.password}
onChange={handleTarget}
placeholder="Contraseña"
/>
{errors.password && <span>{errors.password.message}</span>}
<input
type="password"
name="confirmPassword"
value={target.confirmPassword}
onChange={handleTarget}
placeholder="Confirmar Contraseña"
/>
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
<input
type="number"
name="age"
value={target.age}
onChange={handleTarget}
placeholder="Edad"
/>
{errors.age && <span>{errors.age.message}</span>}
<label>
<input
type="checkbox"
name="terms"
checked={target.terms}
onChange={handleTarget}
/>
Acepto los términos
</label>
{errors.terms && <span>{errors.terms.message}</span>}
<button type="submit">Registrarse</button>
</form>
);
}- ✅ Sin instalación adicional - Incluido en el paquete
- ✅ Detección automática - El hook reconoce schemas de Zod
- ✅ Transformaciones - Convierte datos automáticamente
- ✅ Validaciones complejas -
.refine()para lógica custom - ✅ Inferencia de tipos - TypeScript automático con
z.infer - ✅ Validación cross-field - Comparar múltiples campos
useTargetHandler<T>(
initialValues: T,
validationRules: ValidationRules<T> | ZodSchema<T>,
storageOptions?: StorageOptions,
securityOptions?: SecurityOptions,
httpHook?: typeof useHttpRequest
): [
target: T,
handleTarget: (e: ChangeEvent<HTMLInputElement>) => void,
handleSubmit: (callback: (data: T) => void) => (e: FormEvent) => void,
errors: ValidationErrors<T>,
httpRequest?: HttpRequestReturn
]Valores iniciales del formulario.
{ email: "", password: "", age: 0 }Reglas de validación nativas o schema de Zod.
ValidationRules:
{
email: {
required: true,
requiredMessage: "Email requerido",
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
patternMessage: "Email inválido"
},
password: {
required: true,
minLength: 8,
minLengthMessage: "Mínimo 8 caracteres",
maxLength: 100
},
age: {
min: 18,
max: 99
},
terms: {
checked: true,
checkedMessage: "Debes aceptar los términos"
},
confirmPassword: {
matches: "password",
matchMessage: "Las contraseñas no coinciden"
}
}Zod Schema:
z.object({
email: z.string().email(),
password: z.string().min(8)
}){
storageType: "local" | "session" | "none", // default: "none"
storageKey: string // default: "formData"
}{
enableCSRF: boolean, // default: false
rateLimit: number // milliseconds, default: 1000
}Hook opcional para funcionalidades HTTP.
Estado actual del formulario.
Manejador de cambios para inputs.
Manejador de envío del formulario con validación.
Objeto con errores de validación.
{
email?: { message: string },
password?: { message: string }
}Funcionalidades HTTP (si se pasa useHttpRequest).
{
apiCall: (method, endpoint, data?) => Promise<void>,
apiResponse: any,
error: Error | null,
isLoading: boolean,
userFound: boolean,
SentryError: (message, context) => void,
SentryWarning: (message, context) => void,
SentryInfo: (message, context) => void,
SentryEvent: (event, context) => void
}| Amenaza | Protección Frontend | Nivel | Requiere Backend |
|---|---|---|---|
| XSS | ✅ DOMPurify | Alto | |
| CSRF | Medio | ✅ Obligatorio | |
| SQL Injection | ❌ No protege | N/A | ✅ Obligatorio |
El hook usa DOMPurify para sanitizar inputs de tipo text, email, tel, url.
Elimina:
- Tags
<script>y contenido malicioso - Event handlers (
onerror,onload,onclick) - JavaScript protocol (
javascript:,data:URIs) - Inyecciones SVG/XML
Campos NO sanitizados:
password,number,textarea,select
Mejores Prácticas:
- Sanitizar también en el backend
- Usar Content Security Policy (CSP)
- Escapar al renderizar HTML
Con enableCSRF=true, el hook incluye automáticamente el token CSRF en headers X-CSRF-Token.
Requiere configuración backend:
// Express.js
import csrf from "csurf";
import cookieParser from "cookie-parser";
app.use(cookieParser());
app.use(csrf({
cookie: {
httpOnly: false,
sameSite: "strict",
secure: true
}
}));
app.get("/api/csrf-token", (req, res) => {
res.cookie("csrfToken", req.csrfToken());
res.json({ success: true });
});// ✅ CORRECTO - Prepared statement
db.query("SELECT * FROM users WHERE email = ?", [email]);
// ❌ VULNERABLE
db.query(`SELECT * FROM users WHERE email = '${email}'`);Usa:
- Prepared Statements
- ORMs (Sequelize, Prisma, TypeORM)
- Validación y escape en servidor
Frontend:
- Sanitización XSS (DOMPurify)
- Token CSRF en headers
- Rate limiting
- Validación de inputs
Backend:
- Validar tokens CSRF
- Prepared statements / ORMs
- Sanitizar inputs
- HTTPS en producción
- CORS configurado
- Rate limiting en servidor
- ✅ Usa Zod para validaciones complejas
- ✅ Combina validación frontend y backend
- ✅ Mensajes de error claros y específicos
- ✅ Valida en tiempo real para mejor UX
- ✅ Habilita CSRF para formularios mutantes
- ✅ Configura rate limiting apropiado
- ✅ Sanitiza inputs en backend también
- ✅ Usa HTTPS en producción
- ✅ Usa
storageType: "session"para formularios temporales - ✅ Limpia localStorage periódicamente
- ✅ Debounce validaciones costosas si es necesario
- ✅ Define interfaces para tus formularios
- ✅ Usa
z.infercon Zod para inferencia automática - ✅ Tipifica callbacks de submit
- CHANGELOG - Historial de versiones
- FUNCTIONALITY - Documentación detallada de características
- TYPESCRIPT MIGRATION - Guía de migración a TypeScript
- NPM Package - Ver en npm
- GitHub Repository - Código fuente
Este proyecto está licenciado bajo los términos especificados en el repositorio.
Las contribuciones son bienvenidas. Por favor, consulta el repositorio para más detalles.
Hecho con ❤️ para la comunidad React
