Structure: - control-plane/: Registries, SIMCO directives, CI/CD templates - projects/: Gamilit, ERP-Suite, Trading-Platform, Betting-Analytics - shared/: Libs catalog, knowledge-base Key features: - Centralized port, domain, database, and service registries - 23 SIMCO directives + 6 fundamental principles - NEXUS agent profiles with delegation rules - Validation scripts for workspace integrity - Dockerfiles for all services - Path aliases for quick reference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
15 KiB
15 KiB
Patrones de Custom Hooks - Frontend GAMILIT
Fecha de creacion: 2025-11-29 Version: 1.0 Estado: VIGENTE Contexto: Estandarizacion de 30+ custom hooks
1. Principios Fundamentales
1.1 Proposito de Custom Hooks
- Encapsular logica reutilizable: Extraer logica de componentes
- Compartir estado: Entre componentes sin prop drilling
- Abstraer complejidad: Ocultar detalles de implementacion
- Testear aisladamente: Logica separada de UI
1.2 Regla de Extraccion
Si una logica con hooks se repite en 2+ componentes, crea un custom hook.
1.3 Naming Convention
- Siempre empezar con
use:useAuth,useForm,useDebounce - Nombre descriptivo de lo que hace:
useUserData,useLocalStorage - Verbos para acciones:
useFetchUser,useSubmitForm
2. Anatomia de un Custom Hook
2.1 Estructura Estandar
/**
* useHookName
* @description Breve descripcion del hook
* @param {ParamType} param - Descripcion del parametro
* @returns {ReturnType} Descripcion del retorno
*
* @example
* const { data, isLoading } = useHookName(param);
*/
import { useState, useEffect, useCallback } from 'react';
// 1. Tipos
interface UseHookNameParams {
initialValue?: string;
onSuccess?: (data: Data) => void;
}
interface UseHookNameReturn {
data: Data | null;
isLoading: boolean;
error: Error | null;
refetch: () => void;
}
// 2. Hook principal
export function useHookName({
initialValue = '',
onSuccess,
}: UseHookNameParams = {}): UseHookNameReturn {
// 2.1 Estado
const [data, setData] = useState<Data | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// 2.2 Efectos
useEffect(() => {
// Logica de efecto
}, [/* dependencias */]);
// 2.3 Callbacks
const refetch = useCallback(() => {
// Logica de refetch
}, [/* dependencias */]);
// 2.4 Return
return {
data,
isLoading,
error,
refetch,
};
}
2.2 Archivo de Hook Simple
// hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number = 300): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
3. Estructura de Carpetas
3.1 Hooks Compartidos
shared/
└── hooks/
├── index.ts # Barrel export
├── useDebounce.ts
├── useLocalStorage.ts
├── useMediaQuery.ts
├── useClickOutside.ts
└── useAsync.ts
3.2 Hooks por Feature
features/
└── auth/
└── hooks/
├── index.ts
├── useAuth.ts
├── useLogin.ts
└── useLogout.ts
3.3 Barrel Export
// shared/hooks/index.ts
export { useDebounce } from './useDebounce';
export { useLocalStorage } from './useLocalStorage';
export { useMediaQuery } from './useMediaQuery';
export { useClickOutside } from './useClickOutside';
export { useAsync } from './useAsync';
// types
export type { UseAsyncReturn } from './useAsync';
4. Patrones Comunes
4.1 State Management Hook
// useToggle.ts
import { useState, useCallback } from 'react';
export function useToggle(initialValue: boolean = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// Uso
const { value: isOpen, toggle, setFalse: close } = useToggle();
4.2 Data Fetching Hook (con React Query)
// useUserData.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchUser, updateUser } from '@/services/api/userApi';
import type { User, UpdateUserDto } from '@shared/types';
export function useUserData(userId: string) {
const queryClient = useQueryClient();
// Query
const query = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId,
staleTime: 5 * 60 * 1000, // 5 minutos
});
// Mutation
const mutation = useMutation({
mutationFn: (data: UpdateUserDto) => updateUser(userId, data),
onSuccess: (updatedUser) => {
queryClient.setQueryData(['user', userId], updatedUser);
},
});
return {
user: query.data,
isLoading: query.isLoading,
isError: query.isError,
error: query.error,
updateUser: mutation.mutate,
isUpdating: mutation.isPending,
};
}
// Uso
const { user, isLoading, updateUser } = useUserData(userId);
4.3 Form Hook
// useForm.ts
import { useState, useCallback, ChangeEvent, FormEvent } from 'react';
interface UseFormOptions<T> {
initialValues: T;
validate?: (values: T) => Partial<Record<keyof T, string>>;
onSubmit: (values: T) => void | Promise<void>;
}
export function useForm<T extends Record<string, any>>({
initialValues,
validate,
onSubmit,
}: UseFormOptions<T>) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = useCallback((
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
// Clear error when field changes
if (errors[name as keyof T]) {
setErrors(prev => ({ ...prev, [name]: undefined }));
}
}, [errors]);
const handleSubmit = useCallback(async (e: FormEvent) => {
e.preventDefault();
// Validate
if (validate) {
const validationErrors = validate(values);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
}
// Submit
setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
}, [values, validate, onSubmit]);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
}, [initialValues]);
return {
values,
errors,
isSubmitting,
handleChange,
handleSubmit,
reset,
setValues,
setErrors,
};
}
// Uso
const form = useForm({
initialValues: { email: '', password: '' },
validate: (values) => {
const errors: Record<string, string> = {};
if (!values.email) errors.email = 'Requerido';
return errors;
},
onSubmit: async (values) => {
await login(values);
},
});
4.4 Storage Hook
// useLocalStorage.ts
import { useState, useEffect, useCallback } from 'react';
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
// Lazy initialization
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
// Update localStorage when value changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(`Error saving to localStorage: ${key}`, error);
}
}, [key, storedValue]);
// Remove from localStorage
const remove = useCallback(() => {
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.error(`Error removing from localStorage: ${key}`, error);
}
}, [key, initialValue]);
return [storedValue, setStoredValue, remove];
}
// Uso
const [theme, setTheme, clearTheme] = useLocalStorage('theme', 'light');
4.5 Event Listener Hook
// useClickOutside.ts
import { useEffect, useRef, RefObject } from 'react';
export function useClickOutside<T extends HTMLElement>(
handler: () => void
): RefObject<T> {
const ref = useRef<T>(null);
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
const el = ref.current;
if (!el || el.contains(event.target as Node)) {
return;
}
handler();
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [handler]);
return ref;
}
// Uso
const ref = useClickOutside<HTMLDivElement>(() => setIsOpen(false));
<div ref={ref}>...</div>
5. Integracion con React Query
5.1 Pattern para Queries
// usePaginatedData.ts
import { useQuery, keepPreviousData } from '@tanstack/react-query';
interface UsePaginatedDataParams {
page: number;
limit: number;
filters?: Record<string, any>;
}
export function usePaginatedData<T>({
page,
limit,
filters,
}: UsePaginatedDataParams) {
return useQuery({
queryKey: ['data', { page, limit, ...filters }],
queryFn: () => fetchPaginatedData<T>({ page, limit, filters }),
placeholderData: keepPreviousData, // Mantener datos anteriores
staleTime: 30_000, // 30 segundos
});
}
5.2 Pattern para Mutations
// useCreateItem.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-hot-toast';
export function useCreateItem() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createItem,
onSuccess: () => {
// Invalidar queries relacionadas
queryClient.invalidateQueries({ queryKey: ['items'] });
toast.success('Item creado exitosamente');
},
onError: (error: Error) => {
toast.error(`Error: ${error.message}`);
},
});
}
6. Error Handling en Hooks
6.1 Try-Catch Pattern
export function useAsyncAction<T, P extends any[]>(
action: (...args: P) => Promise<T>
) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [data, setData] = useState<T | null>(null);
const execute = useCallback(async (...args: P) => {
setIsLoading(true);
setError(null);
try {
const result = await action(...args);
setData(result);
return result;
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error');
setError(error);
throw error;
} finally {
setIsLoading(false);
}
}, [action]);
return { execute, isLoading, error, data };
}
6.2 Error Boundary Integration
export function useErrorHandler() {
const [error, setError] = useState<Error | null>(null);
// Si hay error, lo propaga al Error Boundary mas cercano
if (error) {
throw error;
}
const handleError = useCallback((error: Error) => {
setError(error);
}, []);
const clearError = useCallback(() => {
setError(null);
}, []);
return { handleError, clearError };
}
7. Dependencies Array
7.1 Reglas de Dependencias
// SIEMPRE incluir todas las dependencias usadas
// MAL - Falta dependencia
useEffect(() => {
fetchData(userId); // userId no esta en deps
}, []); // eslint warning!
// BIEN - Todas las dependencias
useEffect(() => {
fetchData(userId);
}, [userId]);
// useCallback con dependencias correctas
const handleClick = useCallback(() => {
doSomething(value);
}, [value]); // value incluido
7.2 Evitar Dependencias Inestables
// MAL - Objeto nuevo en cada render
useEffect(() => {
fetchData(options);
}, [options]); // options es nuevo cada vez
// BIEN - Dependencias primitivas
useEffect(() => {
fetchData({ page, limit });
}, [page, limit]); // primitivos estables
// O usar useMemo para estabilizar
const stableOptions = useMemo(() => ({ page, limit }), [page, limit]);
useEffect(() => {
fetchData(stableOptions);
}, [stableOptions]);
8. Testing de Hooks
8.1 Setup Basico
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
});
it('should increment', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should reset to initial value', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(5);
});
});
8.2 Testing con React Query
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useUserData } from './useUserData';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
describe('useUserData', () => {
it('should fetch user data', async () => {
const { result } = renderHook(() => useUserData('123'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.user).toBeDefined();
});
});
9. Anti-patrones a Evitar
9.1 NO: Llamar Hooks Condicionalmente
// MAL - Viola reglas de hooks
if (condition) {
const [state] = useState(); // Error!
}
// BIEN - Hook siempre se llama
const [state] = useState();
if (condition) {
// usar state
}
9.2 NO: Efectos sin Cleanup
// MAL - Memory leak potencial
useEffect(() => {
const subscription = subscribe(callback);
// Falta cleanup!
}, []);
// BIEN - Con cleanup
useEffect(() => {
const subscription = subscribe(callback);
return () => subscription.unsubscribe();
}, []);
9.3 NO: Dependencias Faltantes
// MAL - eslint-disable para silenciar warning
useEffect(() => {
doSomething(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // NO hacer esto!
// BIEN - Incluir o refactorizar
useEffect(() => {
doSomething(value);
}, [value]);
10. Checklist de Creacion de Hook
Antes de crear un nuevo custom hook:
- Verificar que no existe hook similar
- Nombre empieza con
use - Parametros tipados con interface
- Return tipado con interface o inline
- Documentacion JSDoc con @example
- Dependencies array completo
- Cleanup en useEffect si necesario
- Al menos 1 test basico
- Exportar en barrel file
11. Referencias
- Componentes:
docs/95-guias-desarrollo/frontend/COMPONENT-PATTERNS.md - Types:
docs/95-guias-desarrollo/frontend/TYPES-CONVENTIONS.md - React Hooks: https://react.dev/reference/react/hooks
- React Query: https://tanstack.com/query/latest
12. Changelog
| Version | Fecha | Cambios |
|---|---|---|
| 1.0 | 2025-11-29 | Creacion inicial |