workspace/projects/gamilit/docs/95-guias-desarrollo/frontend/COMPONENT-PATTERNS.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- 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>
2025-12-08 10:44:23 -06:00

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:

  1. Verificar que no existe componente similar
  2. Determinar si es shared o feature-specific
  3. Crear interface de Props en archivo separado
  4. Usar nombres descriptivos (verbo + sustantivo)
  5. Implementar estados: loading, error, empty, success
  6. Agregar memo si es puro y costoso
  7. Escribir al menos 1 test basico
  8. Exportar en barrel file

12. Referencias


13. Changelog

Version Fecha Cambios
1.0 2025-11-29 Creacion inicial