- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
12 KiB
12 KiB
Patrones de Componentes - Frontend GAMILIT
Fecha de creacion: 2025-11-29 Version: 1.0 Estado: VIGENTE Contexto: Estandarizacion de 180+ componentes React
1. Principios Fundamentales
1.1 Filosofia de Componentes
- Componentes pequenos y enfocados: Una responsabilidad por componente
- Composicion sobre herencia: Componer componentes simples
- Props explicitas: Siempre tipar props con interfaces
- Colocacion: Mantener archivos relacionados juntos
1.2 Regla de Oro
Si un componente supera las 150 lineas, probablemente debe dividirse.
2. Anatomia de un Componente Estandar
2.1 Estructura de Archivo
/**
* ComponentName
* @description Breve descripcion del componente
* @see Backend: [ruta si aplica]
*/
// 1. Imports externos
import { useState, useCallback, memo } from 'react';
import { useQuery } from '@tanstack/react-query';
// 2. Imports internos (types, utils, hooks)
import type { ComponentProps } from './ComponentName.types';
import { formatDate } from '@shared/utils';
import { useAuth } from '@features/auth/hooks';
// 3. Imports de componentes
import { Button, Card } from '@shared/components/ui';
import { LoadingSpinner } from '@shared/components/feedback';
// 4. Constantes locales (si son pocas)
const DEFAULT_PAGE_SIZE = 10;
// 5. Componente principal
export const ComponentName = memo(function ComponentName({
prop1,
prop2,
onAction,
}: ComponentProps) {
// 5.1 Hooks (siempre al inicio)
const { user } = useAuth();
const [state, setState] = useState<string>('');
// 5.2 Queries/Mutations
const { data, isLoading } = useQuery({...});
// 5.3 Handlers (useCallback para optimizar)
const handleClick = useCallback(() => {
onAction?.(state);
}, [state, onAction]);
// 5.4 Early returns (loading, error, empty)
if (isLoading) return <LoadingSpinner />;
if (!data) return null;
// 5.5 Render principal
return (
<Card>
<h2>{prop1}</h2>
<Button onClick={handleClick}>
{prop2}
</Button>
</Card>
);
});
// 6. Display name (para debugging)
ComponentName.displayName = 'ComponentName';
2.2 Archivo de Types
// ComponentName.types.ts
export interface ComponentProps {
/** Descripcion de prop1 */
prop1: string;
/** Descripcion de prop2 */
prop2: string;
/** Callback cuando se ejecuta accion */
onAction?: (value: string) => void;
/** Clases CSS adicionales */
className?: string;
/** Contenido hijo */
children?: React.ReactNode;
}
export interface ComponentState {
isOpen: boolean;
selectedId: string | null;
}
3. Estructura de Carpetas por Feature
3.1 Componente Simple (1 archivo)
features/auth/components/
└── LoginButton.tsx
3.2 Componente Complejo (carpeta)
features/auth/components/LoginForm/
├── index.ts # Re-export
├── LoginForm.tsx # Componente principal
├── LoginForm.types.ts # Types e interfaces
├── LoginForm.styles.ts # Styled components (si aplica)
├── LoginForm.test.tsx # Tests
└── useLoginForm.ts # Hook especifico (si aplica)
3.3 index.ts (Barrel Export)
// features/auth/components/LoginForm/index.ts
export { LoginForm } from './LoginForm';
export type { LoginFormProps } from './LoginForm.types';
4. Props Patterns
4.1 Props Basicas
interface ButtonProps {
// Requeridas
label: string;
onClick: () => void;
// Opcionales con defaults
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
// Children
children?: React.ReactNode;
// ClassName para extension
className?: string;
}
// Uso con defaults
export const Button = ({
label,
onClick,
variant = 'primary',
size = 'md',
disabled = false,
className,
}: ButtonProps) => { ... };
4.2 Props Compuestas
// Extender props de elemento HTML
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
// Uso
export const Input = ({ label, error, ...inputProps }: InputProps) => (
<div>
<label>{label}</label>
<input {...inputProps} />
{error && <span className="error">{error}</span>}
</div>
);
4.3 Render Props
interface DataListProps<T> {
data: T[];
renderItem: (item: T, index: number) => React.ReactNode;
renderEmpty?: () => React.ReactNode;
}
export function DataList<T>({
data,
renderItem,
renderEmpty,
}: DataListProps<T>) {
if (data.length === 0 && renderEmpty) {
return <>{renderEmpty()}</>;
}
return <ul>{data.map(renderItem)}</ul>;
}
5. Composicion de Componentes
5.1 Compound Components
// Card.tsx
interface CardProps {
children: React.ReactNode;
}
interface CardHeaderProps {
title: string;
action?: React.ReactNode;
}
export const Card = ({ children }: CardProps) => (
<div className="card">{children}</div>
);
Card.Header = ({ title, action }: CardHeaderProps) => (
<div className="card-header">
<h3>{title}</h3>
{action}
</div>
);
Card.Body = ({ children }: { children: React.ReactNode }) => (
<div className="card-body">{children}</div>
);
Card.Footer = ({ children }: { children: React.ReactNode }) => (
<div className="card-footer">{children}</div>
);
// Uso
<Card>
<Card.Header title="Titulo" action={<Button>Accion</Button>} />
<Card.Body>Contenido</Card.Body>
<Card.Footer>Footer</Card.Footer>
</Card>
5.2 HOCs (Higher Order Components)
// withAuth.tsx
export function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function AuthenticatedComponent(props: P) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <WrappedComponent {...props} />;
};
}
// Uso
export const ProtectedPage = withAuth(DashboardPage);
6. Performance Patterns
6.1 memo para Componentes Puros
import { memo } from 'react';
// Usar memo cuando:
// - Props no cambian frecuentemente
// - Componente es costoso de renderizar
// - Lista de items grandes
export const UserCard = memo(function UserCard({ user }: UserCardProps) {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
6.2 useMemo para Calculos Costosos
import { useMemo } from 'react';
export const Dashboard = ({ data }: DashboardProps) => {
// Calcular solo cuando data cambia
const processedData = useMemo(() => {
return data.map(item => ({
...item,
computed: expensiveCalculation(item),
}));
}, [data]);
return <Chart data={processedData} />;
};
6.3 useCallback para Handlers Estables
import { useCallback } from 'react';
export const Form = ({ onSubmit }: FormProps) => {
const [value, setValue] = useState('');
// Estable, no se recrea en cada render
const handleSubmit = useCallback((e: FormEvent) => {
e.preventDefault();
onSubmit(value);
}, [value, onSubmit]);
return <form onSubmit={handleSubmit}>...</form>;
};
6.4 Lazy Loading
import { lazy, Suspense } from 'react';
// Cargar componente pesado solo cuando se necesita
const HeavyChart = lazy(() => import('./HeavyChart'));
export const Dashboard = () => (
<Suspense fallback={<LoadingSpinner />}>
<HeavyChart />
</Suspense>
);
7. Error Handling
7.1 Error Boundary
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught:', error, errorInfo);
// Enviar a servicio de monitoreo
}
render() {
if (this.state.hasError) {
return this.props.fallback || <DefaultErrorFallback />;
}
return this.props.children;
}
}
// Uso
<ErrorBoundary fallback={<ErrorPage />}>
<RiskyComponent />
</ErrorBoundary>
7.2 Manejo de Estados Async
export const UserProfile = ({ userId }: { userId: string }) => {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// Estado de carga
if (isLoading) {
return <ProfileSkeleton />;
}
// Estado de error
if (isError) {
return <ErrorMessage error={error} />;
}
// Estado vacio
if (!data) {
return <EmptyState message="Usuario no encontrado" />;
}
// Render con datos
return <ProfileCard user={data} />;
};
8. Patrones de Estado
8.1 Estado Local vs Global
| Tipo de Estado | Donde Guardarlo |
|---|---|
| UI temporal (modals, dropdowns) | useState local |
| Datos del formulario | useState o react-hook-form |
| Datos del servidor | React Query |
| Estado compartido entre features | Zustand store |
| Estado de autenticacion | AuthStore (Zustand) |
8.2 Elevacion de Estado
// Estado elevado al padre cuando multiples hijos lo necesitan
const Parent = () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<>
<List onSelect={setSelectedId} />
<Detail selectedId={selectedId} />
</>
);
};
9. Testing de Componentes
9.1 Estructura de Test
// ComponentName.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { ComponentName } from './ComponentName';
describe('ComponentName', () => {
// Setup comun
const defaultProps = {
prop1: 'test',
onAction: vi.fn(),
};
it('renders correctly', () => {
render(<ComponentName {...defaultProps} />);
expect(screen.getByText('test')).toBeInTheDocument();
});
it('calls onAction when clicked', async () => {
render(<ComponentName {...defaultProps} />);
await fireEvent.click(screen.getByRole('button'));
expect(defaultProps.onAction).toHaveBeenCalled();
});
it('shows loading state', () => {
render(<ComponentName {...defaultProps} isLoading />);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
});
9.2 Testing de Hooks
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments correctly', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
10. Anti-patrones a Evitar
10.1 NO: Props Drilling Profundo
// MAL
<GrandParent data={data}>
<Parent data={data}>
<Child data={data}>
<GrandChild data={data} /> {/* 4 niveles! */}
</Child>
</Parent>
</GrandParent>
// BIEN: Usar Context o Zustand
const DataContext = createContext<Data | null>(null);
10.2 NO: Logica en JSX
// MAL
<div>
{items.filter(x => x.active).map(x => (
<Item key={x.id} data={processData(x)} />
))}
</div>
// BIEN: Extraer logica
const activeItems = useMemo(() =>
items.filter(x => x.active).map(processData),
[items]
);
<div>
{activeItems.map(item => <Item key={item.id} data={item} />)}
</div>
10.3 NO: Componentes Dios (>300 lineas)
// MAL: Un componente hace todo
const Dashboard = () => { /* 500 lineas */ };
// BIEN: Dividir en subcomponentes
const Dashboard = () => (
<>
<DashboardHeader />
<DashboardStats />
<DashboardCharts />
<DashboardActivity />
</>
);
11. Checklist de Creacion de Componente
Antes de crear un nuevo componente:
- Verificar que no existe componente similar
- Determinar si es shared o feature-specific
- Crear interface de Props en archivo separado
- Usar nombres descriptivos (verbo + sustantivo)
- Implementar estados: loading, error, empty, success
- Agregar memo si es puro y costoso
- Escribir al menos 1 test basico
- Exportar en barrel file
12. Referencias
- Hooks:
docs/95-guias-desarrollo/frontend/HOOK-PATTERNS.md - Types:
docs/95-guias-desarrollo/frontend/TYPES-CONVENTIONS.md - React Testing Library: https://testing-library.com/docs/react-testing-library/intro
- React Patterns: https://reactpatterns.com/
13. Changelog
| Version | Fecha | Cambios |
|---|---|---|
| 1.0 | 2025-11-29 | Creacion inicial |