DEV Community

Jesus Oviedo Riquelme
Jesus Oviedo Riquelme

Posted on

LLMZ25-4 Review : Mejorando React + TailwindCSS

Siguiendo nuestra discusión previa sobre React y TailwindCSS en QuizMate, este post explora cómo mejorar las implementaciones de estas tecnologías, ya sea que estés empezando desde el enfoque de QuizMate o integrándolas en proyectos como lus-laboris-py.

Cubriremos mejoras listas para producción, optimizaciones de rendimiento y patrones profesionales que elevan tus aplicaciones React + TailwindCSS.


Análisis del Estado Actual: QuizMate

Lo que QuizMate Hace Bien ✅

  1. Configuración simple - Enfoque basado en CDN elimina la complejidad del build
  2. Arquitectura basada en componentes - Separación limpia de responsabilidades
  3. Clases de utilidad de TailwindCSS - Estilizado rápido sin CSS personalizado
  4. Manejo de estado reactivo - useEffect y useState para flujo de datos
  5. Buenos patrones de UX - Estados de carga, manejo de errores, renderizado condicional

Áreas para Mejorar 📈

  1. Optimizaciones de rendimiento
  2. Organización de código y reutilización
  3. Proceso de build para producción
  4. Seguridad de tipos y límites de errores
  5. Estrategias de testing

Mejora 1: Migrar a una Configuración con Build

Enfoque Actual (CDN)

<!-- Simple pero limitado -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

Enfoque Mejorado (Con Build)

Paso 1: Inicializar un Proyecto React Moderno

# Usando Vite (Recomendado - Rápido y Moderno)
npm create vite@latest quizmate-frontend -- --template react

# O usando Create React App
npx create-react-app quizmate-frontend
Enter fullscreen mode Exit fullscreen mode

Paso 2: Instalar TailwindCSS

cd quizmate-frontend
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Paso 3: Configurar TailwindCSS

tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        'quiz-blue': '#3B82F6',
        'quiz-green': '#10B981',
      },
    },
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Paso 4: Agregar Directivas de TailwindCSS

src/index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn-primary {
    @apply bg-blue-600 text-white px-4 py-2 rounded-md 
           font-medium hover:bg-blue-700 transition-colors 
           disabled:bg-gray-300 disabled:cursor-not-allowed;
  }

  .card {
    @apply bg-white rounded-lg shadow-md p-6;
  }
}
Enter fullscreen mode Exit fullscreen mode

Beneficios:

  • ✅ Tree-shaking elimina CSS no usado
  • ✅ Carga de páginas más rápida
  • ✅ Mejor experiencia de desarrollo (hot reload)
  • ✅ Puedes usar TypeScript
  • ✅ Tamaños de bundle más pequeños

Mejora 2: Extracción de Componentes y Reutilización

Código Actual (Todo en un Archivo)

function App() {
    // 500+ líneas de código en un archivo
    return <div>...</div>;
}
Enter fullscreen mode Exit fullscreen mode

Estructura Mejorada

src/
├── components/
│   ├── common/
│   │   ├── Button.jsx
│   │   ├── Card.jsx
│   │   ├── Input.jsx
│   │   └── LoadingSpinner.jsx
│   ├── quiz/
│   │   ├── FileUpload.jsx
│   │   ├── QuizCreator.jsx
│   │   └── QuizTaker.jsx
│   └── layout/
│       ├── Header.jsx
│       └── Footer.jsx
├── hooks/
│   ├── useQuizGeneration.js
│   ├── useFileUpload.js
│   └── useSources.js
├── services/
│   └── api.js
└── utils/
    └── validation.js
Enter fullscreen mode Exit fullscreen mode

Ejemplo: Componente Botón Reutilizable

src/components/common/Button.jsx:

import PropTypes from 'prop-types';

export const Button = ({ 
    children, 
    onClick, 
    disabled = false, 
    variant = 'primary',
    size = 'md',
    className = '',
    ...props 
}) => {
    const clasesBase = 'font-medium rounded-md transition-colors';

    const clasesVariante = {
        primary: 'bg-blue-600 text-white hover:bg-blue-700',
        secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
        success: 'bg-green-600 text-white hover:bg-green-700',
        danger: 'bg-red-600 text-white hover:bg-red-700',
    };

    const clasesTamaño = {
        sm: 'px-3 py-1.5 text-sm',
        md: 'px-4 py-2 text-base',
        lg: 'px-6 py-3 text-lg',
    };

    const clasesDeshabilitado = 'disabled:bg-gray-300 disabled:cursor-not-allowed';

    return (
        <button
            onClick={onClick}
            disabled={disabled}
            className={`${clasesBase} ${clasesVariante[variant]} 
                       ${clasesTamaño[size]} ${clasesDeshabilitado} ${className}`}
            {...props}
        >
            {children}
        </button>
    );
};

Button.propTypes = {
    children: PropTypes.node.isRequired,
    onClick: PropTypes.func,
    disabled: PropTypes.bool,
    variant: PropTypes.oneOf(['primary', 'secondary', 'success', 'danger']),
    size: PropTypes.oneOf(['sm', 'md', 'lg']),
    className: PropTypes.string,
};
Enter fullscreen mode Exit fullscreen mode

Uso:

import { Button } from './components/common/Button';

<Button 
    onClick={handleClick}
    variant="primary"
    size="md"
    disabled={isLoading}
>
    Generar Quiz
</Button>
Enter fullscreen mode Exit fullscreen mode

Mejora 3: Hooks Personalizados para Separación de Lógica

Código Actual (Lógica Mezclada con UI)

function QuizCreator() {
    const [sources, setSources] = useState([]);
    const [generating, setGenerating] = useState(false);

    // Lógica de API mezclada con UI
    const fetchSources = async () => {
        // ...
    };

    return <div>...</div>;
}
Enter fullscreen mode Exit fullscreen mode

Mejorado: Extraer a Hook Personalizado

src/hooks/useQuizGeneration.js:

import { useState, useEffect } from 'react';

export const useQuizGeneration = () => {
    const [sources, setSources] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        fetchSources();
    }, []);

    const fetchSources = async () => {
        try {
            setLoading(true);
            const response = await fetch('/ingestion/sources');
            if (!response.ok) throw new Error('Falló al obtener fuentes');
            const data = await response.json();
            setSources(data);
        } catch (err) {
            setError(err.message);
        } finally {
            setLoading(false);
        }
    };

    const generateQuiz = async (config) => {
        try {
            setLoading(true);
            const response = await fetch('/api/quiz', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(config),
            });

            if (!response.ok) throw new Error('Falló al generar quiz');

            const data = await response.json();
            return data.quiz;
        } catch (err) {
            setError(err.message);
            throw err;
        } finally {
            setLoading(false);
        }
    };

    return {
        sources,
        loading,
        error,
        generateQuiz,
        refetch: fetchSources,
    };
};
Enter fullscreen mode Exit fullscreen mode

Uso:

import { useQuizGeneration } from './hooks/useQuizGeneration';

function QuizCreator() {
    const { sources, loading, error, generateQuiz } = useQuizGeneration();
    const [query, setQuery] = useState('');

    const handleGenerate = async () => {
        try {
            const quiz = await generateQuiz({
                query: query.trim(),
                numberOfQuestions: 5,
            });
            onQuizGenerated(quiz);
        } catch (err) {
            console.error('Falló al generar quiz:', err);
        }
    };

    return (
        <div>
            {loading && <LoadingSpinner />}
            {error && <ErrorMessage>{error}</ErrorMessage>}
            {/* Componentes UI */}
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Beneficios:

  • ✅ Lógica reutilizable
  • ✅ Testeable de forma aislada
  • ✅ Código de componente más limpio
  • ✅ Mejor manejo de errores

Mejora 4: Capa de Servicio API

Código Actual (Llamadas Fetch Inline)

const response = await fetch('/api/quiz', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({...})
});
const data = await response.json();
Enter fullscreen mode Exit fullscreen mode

Mejorado: Servicio API Centralizado

src/services/api.js:

const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080';

class ApiService {
    async request(endpoint, options = {}) {
        const url = `${API_BASE_URL}${endpoint}`;
        const config = {
            headers: {
                'Content-Type': 'application/json',
                ...options.headers,
            },
            ...options,
        };

        try {
            const response = await fetch(url, config);

            if (!response.ok) {
                const error = await response.json().catch(() => ({}));
                throw new Error(error.message || `HTTP ${response.status}`);
            }

            const data = await response.json();
            return data;
        } catch (error) {
            console.error(`Error API [${endpoint}]:`, error);
            throw error;
        }
    }

    async post(endpoint, body) {
        return this.request(endpoint, {
            method: 'POST',
            body: JSON.stringify(body),
        });
    }

    async get(endpoint) {
        return this.request(endpoint, { method: 'GET' });
    }

    async uploadFile(endpoint, file, additionalData) {
        const formData = new FormData();
        formData.append('file', file);

        if (additionalData) {
            Object.entries(additionalData).forEach(([key, value]) => {
                formData.append(key, value);
            });
        }

        return this.request(endpoint, {
            method: 'POST',
            body: formData,
            headers: {}, // Dejar que el navegador establezca Content-Type con boundary
        });
    }
}

export const apiService = new ApiService();

// Métodos API específicos
export const quizApi = {
    generate: (config) => apiService.post('/api/quiz', config),
    evaluate: (request) => apiService.post('/api/evaluation/evaluate-query', request),
};

export const ingestionApi = {
    upload: (file, sourceName) => apiService.uploadFile('/ingestion/upload', file, { sourceName }),
    getSources: () => apiService.get('/ingestion/sources'),
};
Enter fullscreen mode Exit fullscreen mode

Uso:

import { quizApi, ingestionApi } from './services/api';

// Limpio y simple
const quiz = await quizApi.generate({ query, source, numberOfQuestions });
const sources = await ingestionApi.getSources();
await ingestionApi.upload(file, sourceName);
Enter fullscreen mode Exit fullscreen mode

Mejora 5: Mejores Prácticas de TailwindCSS

1. Usar @apply para Patrones Repetitivos

En lugar de:

<div className="bg-white rounded-lg shadow-md p-6">...</div>
Enter fullscreen mode Exit fullscreen mode

Crear componentes reutilizables:

/* tailwind.css */
@layer components {
  .card {
    @apply bg-white rounded-lg shadow-md p-6;
  }

  .card-hover {
    @apply card hover:shadow-lg transition-shadow;
  }

  .input-field {
    @apply block w-full px-3 py-2 border border-gray-300 
           rounded-md shadow-sm focus:outline-none 
           focus:ring-blue-500 focus:border-blue-500;
  }
}
Enter fullscreen mode Exit fullscreen mode

Uso:

<div className="card">...</div>
<div className="card-hover">...</div>
<input className="input-field" />
Enter fullscreen mode Exit fullscreen mode

2. Usar Variables CSS para Temas

tailwind.config.js:

export default {
  theme: {
    extend: {
      colors: {
        primary: {
          50: 'var(--color-primary-50)',
          500: 'var(--color-primary-500)',
          900: 'var(--color-primary-900)',
        },
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

3. Organizar con Valores Arbitrarios

// Tamaños de texto responsivos
<div className="text-[clamp(1rem,4vw,2rem)]">
  Texto Adaptativo
</div>

// Cálculos complejos
<div className="w-[calc(100%-2rem)]">
  Ancho Calculado
</div>

// Mejor que hardcoded
<div className="h-[600px]">
  Altura Fija
</div>
Enter fullscreen mode Exit fullscreen mode

4. Usar Modo JIT

Asegurar que JIT está habilitado en tailwind.config.js:

export default {
  mode: 'jit', // Compilación Just-in-Time
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
}
Enter fullscreen mode Exit fullscreen mode

Mejora 6: Límites de Errores y Estados de Carga

Componente Error Boundary

src/components/common/ErrorBoundary.jsx:

import React from 'react';

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false, error: null };
    }

    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }

    componentDidCatch(error, errorInfo) {
        console.error('Error capturado por boundary:', error, errorInfo);
    }

    render() {
        if (this.state.hasError) {
            return (
                <div className="min-h-screen flex items-center justify-center bg-gray-50">
                    <div className="text-center">
                        <h2 className="text-2xl font-bold text-red-600 mb-4">
                            Algo salió mal
                        </h2>
                        <p className="text-gray-600 mb-4">
                            {this.state.error?.message || 'Ocurrió un error inesperado'}
                        </p>
                        <button
                            onClick={() => window.location.reload()}
                            className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
                        >
                            Recargar Página
                        </button>
                    </div>
                </div>
            );
        }

        return this.props.children;
    }
}

export default ErrorBoundary;
Enter fullscreen mode Exit fullscreen mode

Uso:

import ErrorBoundary from './components/common/ErrorBoundary';

function App() {
    return (
        <ErrorBoundary>
            <TusComponentes />
        </ErrorBoundary>
    );
}
Enter fullscreen mode Exit fullscreen mode

Componente de Carga

src/components/common/LoadingSpinner.jsx:

export const LoadingSpinner = ({ size = 'md', className = '' }) => {
    const sizeClasses = {
        sm: 'w-4 h-4',
        md: 'w-8 h-8',
        lg: 'w-12 h-12',
    };

    return (
        <div className={`flex justify-center items-center ${className}`}>
            <div className={`${sizeClasses[size]} border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin`} />
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

Mejora 7: Testing

Configurar Testing

npm install -D vitest @testing-library/react @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode

src/__tests__/Button.test.jsx:

import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '../components/common/Button';

describe('Componente Button', () => {
    it('renderiza con texto', () => {
        render(<Button>Haz Clic</Button>);
        expect(screen.getByText('Haz Clic')).toBeInTheDocument();
    });

    it('llama onClick cuando se hace clic', () => {
        const handleClick = vi.fn();
        render(<Button onClick={handleClick}>Clic</Button>);

        fireEvent.click(screen.getByText('Clic'));
        expect(handleClick).toHaveBeenCalledTimes(1);
    });

    it('está deshabilitado cuando disabled prop es true', () => {
        render(<Button disabled>Clic</Button>);
        expect(screen.getByText('Clic')).toBeDisabled();
    });
});
Enter fullscreen mode Exit fullscreen mode

Aplicar Estas Mejoras a lus-laboris-py

Camino de Migración

Fase 1: Configurar Build Moderno

npm create vite@latest lus-laboris-frontend -- --template react
cd lus-laboris-frontend
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Fase 2: Crear Integración API

npm install axios # o usar fetch
Enter fullscreen mode Exit fullscreen mode

Fase 3: Estructura de Componentes

// api/lusLaborisApi.js
import axios from 'axios';

const api = axios.create({
    baseURL: 'http://localhost:5000', // Tu backend Python
});

export const lusLaborisApi = {
    getJobs: () => api.get('/api/trabajos'),
    getJobById: (id) => api.get(`/api/trabajos/${id}`),
    aplicarTrabajo: (jobId, datosAplicacion) => 
        api.post(`/api/trabajos/${jobId}/aplicar`, datosAplicacion),
};
Enter fullscreen mode Exit fullscreen mode

Fase 4: Migración Gradual

  1. Comenzar con nuevas características en React
  2. Mantener funcionalidad existente funcionando
  3. Migrar página por página
  4. Mantener compatibilidad de API

Despliegue en Producción

Build para Producción

npm run build
# Salida: dist/
Enter fullscreen mode Exit fullscreen mode

Servir con tu Backend

Python (Flask):

from flask import Flask, send_from_directory

app = Flask(__name__, static_folder='../lus-laboris-frontend/dist')

@app.route('/')
def index():
    return send_from_directory('../lus-laboris-frontend/dist', 'index.html')
Enter fullscreen mode Exit fullscreen mode

Python (FastAPI):

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse

app = FastAPI()

app.mount("/static", StaticFiles(directory="../lus-laboris-frontend/dist"), name="static")

@app.get("/")
async def index():
    return FileResponse('../lus-laboris-frontend/dist/index.html')
Enter fullscreen mode Exit fullscreen mode

Puntos Clave

  1. Configuración con build proporciona mejor DX y rendimiento
  2. Extracción de componentes mejora mantenibilidad
  3. Hooks personalizados separan lógica de presentación
  4. Capa de servicio API centraliza comunicación
  5. Mejores prácticas de TailwindCSS optimizan estilizado
  6. Límites de errores mejoran resiliencia
  7. Testing asegura calidad

Comparación: Antes vs Después

Aspecto Enfoque CDN (QuizMate) Enfoque con Build (Mejorado)
Tiempo de Setup < 1 minuto ~5 minutos
Velocidad de Desarrollo Rápido inicialmente Rápido con HMR
Tamaño de Bundle ~500KB (todo React) Optimizado, tree-shaken
Tiempo de Build 0 segundos ~1-2 segundos
Listo para Producción Bueno para prototipos Mejor para producción
Experiencia de Desarrollador Buena Excelente

Conclusión

Mientras el enfoque de CDN de QuizMate es excelente para comenzar rápidamente, migrar a una configuración con build y organización adecuada produce beneficios significativos:

  • Mejor experiencia de desarrollador
  • Rendimiento mejorado
  • Mantenibilidad aumentada
  • Código listo para producción
  • Opciones de seguridad de tipos

Estas mejoras aplican ya sea que estés mejorando QuizMate o construyendo un nuevo frontend para proyectos como lus-laboris-py.

Top comments (0)