# Extender Componentes
Aprende cómo crear componentes personalizados y extender los componentes existentes de Dynamic Framework para adaptarlos a tus necesidades específicas.
# Conceptos Fundamentales
# Composición vs Herencia
Dynamic Framework favorece la composición sobre la herencia:
// Evitar: Herencia directa
class MyButton extends DynamicButton {
// Problemático para mantenimiento
}
// Preferir: Composición
import { DButton, DIcon } from '@dynamic-framework/ui-react';
const MyButton = ({ children, ...props }) => {
return (
<DButton
{...props}
className="my-custom-button"
>
<DIcon icon="arrow-right" />
{children}
</DButton>
);
};
# Extender Componentes Básicos
# Wrapper Components
Crea componentes wrapper que agregan funcionalidad:
// src/components/extended/PrimaryButton.jsx
import React from 'react';
import { DButton } from '@dynamic-framework/ui-react';
import { trackEvent } from '@/utils/analytics';
const PrimaryButton = ({
children,
onClick,
trackingLabel,
size = 'lg',
...props
}) => {
const handleClick = (e) => {
// Agregar analytics
if (trackingLabel) {
trackEvent('button_click', {
label: trackingLabel,
component: 'PrimaryButton'
});
}
// Llamar onClick original
onClick?.(e);
};
return (
<DButton
color="primary"
size={size}
onClick={handleClick}
className="primary-button-extended"
{...props}
>
{children}
</DButton>
);
};
export default PrimaryButton;
# Higher-Order Components (HOCs)
Crea HOCs para agregar funcionalidad transversal:
// src/hocs/withLoading.jsx
import React, { useState } from 'react';
import { DProgress } from '@dynamic-framework/ui-react';
const withLoading = (WrappedComponent) => {
return function WithLoadingComponent(props) {
const [isLoading, setIsLoading] = useState(false);
const startLoading = () => setIsLoading(true);
const stopLoading = () => setIsLoading(false);
if (isLoading) {
return (
<div className="loading-container">
<DProgress />
</div>
);
}
return (
<WrappedComponent
{...props}
isLoading={isLoading}
startLoading={startLoading}
stopLoading={stopLoading}
/>
);
};
};
export default withLoading;
// Uso
const EnhancedForm = withLoading(MyFormComponent);
# Componentes Compuestos
# Crear Componentes Modulares
// src/components/extended/ProductCard/index.jsx
import React from 'react';
import { DCard } from '@dynamic-framework/ui-react';
import ProductHeader from './ProductHeader';
import ProductDetails from './ProductDetails';
import ProductActions from './ProductActions';
const ProductCard = ({ product, onAction }) => {
return (
<DCard className="product-card-extended">
<DCard.Header>
<ProductHeader
name={product.name}
code={product.code}
type={product.type}
/>
</DCard.Header>
<DCard.Body>
<ProductDetails
description={product.description}
features={product.features}
/>
</DCard.Body>
<DCard.Footer>
<ProductActions
productId={product.id}
onAction={onAction}
/>
</DCard.Footer>
</DCard>
);
};
// Exponer sub-componentes para flexibilidad
ProductCard.Header = ProductHeader;
ProductCard.Details = ProductDetails;
ProductCard.Actions = ProductActions;
export default ProductCard;
# Sub-componentes
// src/components/extended/ProductCard/ProductDetails.jsx
import React from 'react';
import { DCurrencyText, DBadge } from '@dynamic-framework/ui-react';
const ProductDetails = ({ description, features, price }) => {
return (
<div className="product-details">
<p className="description">{description}</p>
<div className="features">
{features.map((feature, index) => (
<DBadge key={index} color="info">{feature}</DBadge>
))}
</div>
{price && (
<DCurrencyText value={price} />
)}
</div>
);
};
export default ProductDetails;
# Hooks Personalizados para Componentes
# useComponentState Hook
// src/hooks/useComponentState.js
import { useState, useCallback, useRef } from 'react';
export const useComponentState = (initialState = {}) => {
const [state, setState] = useState(initialState);
const previousStateRef = useRef(initialState);
const updateState = useCallback((updates) => {
setState(prevState => {
const newState = { ...prevState, ...updates };
previousStateRef.current = prevState;
return newState;
});
}, []);
const resetState = useCallback(() => {
setState(initialState);
previousStateRef.current = initialState;
}, [initialState]);
const hasChanged = useCallback((key) => {
return state[key] !== previousStateRef.current[key];
}, [state]);
return {
state,
updateState,
resetState,
hasChanged,
previousState: previousStateRef.current
};
};
# useFormValidation Hook
// src/hooks/useFormValidation.js
import { useState, useCallback } from 'react';
export const useFormValidation = (validationRules) => {
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = useCallback((name, value) => {
const rules = validationRules[name];
if (!rules) return true;
for (const rule of rules) {
const error = rule(value);
if (error) {
setErrors(prev => ({ ...prev, [name]: error }));
return false;
}
}
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[name];
return newErrors;
});
return true;
}, [validationRules]);
const validateAll = useCallback((values) => {
const newErrors = {};
let isValid = true;
Object.keys(validationRules).forEach(name => {
const rules = validationRules[name];
const value = values[name];
for (const rule of rules) {
const error = rule(value);
if (error) {
newErrors[name] = error;
isValid = false;
break;
}
}
});
setErrors(newErrors);
return isValid;
}, [validationRules]);
const markTouched = useCallback((name) => {
setTouched(prev => ({ ...prev, [name]: true }));
}, []);
const resetValidation = useCallback(() => {
setErrors({});
setTouched({});
}, []);
return {
errors,
touched,
validate,
validateAll,
markTouched,
resetValidation,
isValid: Object.keys(errors).length === 0
};
};
# Render Props Pattern
# Componente con Render Props
// src/components/extended/DataProvider.jsx
import React, { useState, useEffect } from 'react';
const DataProvider = ({
loader,
params,
children,
renderLoading,
renderError,
cacheKey
}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
setError(null);
// Verificar caché
if (cacheKey) {
const cached = sessionStorage.getItem(cacheKey);
if (cached) {
setData(JSON.parse(cached));
setLoading(false);
return;
}
}
const result = await loader(params);
setData(result);
// Guardar en caché
if (cacheKey) {
sessionStorage.setItem(cacheKey, JSON.stringify(result));
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
loadData();
}, [loader, params, cacheKey]);
if (loading && renderLoading) {
return renderLoading();
}
if (error && renderError) {
return renderError(error);
}
return children({ data, loading, error, refetch: loadData });
};
// Uso
<DataProvider
loader={accountService.getAccounts}
params={{ userId: currentUser.id }}
renderLoading={() => <DProgress />}
renderError={(error) => <ErrorMessage error={error} />}
>
{({ data: accounts }) => (
<AccountList accounts={accounts} />
)}
</DataProvider>
# Componentes Polimórficos
# Componente que puede renderizar como diferentes elementos
// src/components/extended/Box.jsx
import React from 'react';
import clsx from 'clsx';
const Box = ({
as: Component = 'div',
className,
padding,
margin,
display,
flexDirection,
justifyContent,
alignItems,
gap,
children,
...props
}) => {
const classes = clsx(
'box',
className,
{
[`p-${padding}`]: padding,
[`m-${margin}`]: margin,
[`d-${display}`]: display,
[`flex-${flexDirection}`]: flexDirection,
[`justify-${justifyContent}`]: justifyContent,
[`align-${alignItems}`]: alignItems,
[`gap-${gap}`]: gap,
}
);
return (
<Component className={classes} {...props}>
{children}
</Component>
);
};
// Uso
<Box as="section" display="flex" flexDirection="column" gap={3}>
<Box as="header" padding={2}>
<h1>Título</h1>
</Box>
<Box as="main" padding={4}>
Contenido principal
</Box>
</Box>
# Componentes con Estado Global
# Usando Context API
// src/contexts/ThemeContext.jsx
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const [customColors, setCustomColors] = useState({});
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const updateColors = (colors) => {
setCustomColors(prev => ({ ...prev, ...colors }));
};
const value = {
theme,
customColors,
toggleTheme,
updateColors
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
// Componente que usa el contexto
const ThemedButton = ({ children, ...props }) => {
const { theme, customColors } = useTheme();
return (
<Button
{...props}
style={{
backgroundColor: customColors.primary || 'default',
color: theme === 'dark' ? 'white' : 'black'
}}
>
{children}
</Button>
);
};
# Componentes Lazy y Code Splitting
# Lazy Loading de Componentes Pesados
// src/components/extended/LazyChart.jsx
import React, { lazy, Suspense } from 'react';
import { DProgress } from '@dynamic-framework/ui-react';
// Lazy load del componente pesado
const HeavyChart = lazy(() => import('./HeavyChart'));
const LazyChart = ({ data, ...props }) => {
return (
<Suspense
fallback={
<div className="chart-loading">
<DProgress />
<p>Cargando gráfico...</p>
</div>
}
>
<HeavyChart data={data} {...props} />
</Suspense>
);
};
export default LazyChart;
# Componentes con Animaciones
# Usando Framer Motion
// src/components/extended/AnimatedCard.jsx
import React from 'react';
import { motion } from 'framer-motion';
import { DCard } from '@dynamic-framework/ui-react';
const AnimatedCard = ({ children, delay = 0, ...props }) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{
duration: 0.3,
delay,
ease: 'easeOut'
}}
whileHover={{
scale: 1.02,
boxShadow: '0 10px 30px rgba(0,0,0,0.1)'
}}
>
<DCard {...props}>
{children}
</DCard>
</motion.div>
);
};
export default AnimatedCard;
# Testing de Componentes Extendidos
# Test de Componente Wrapper
// src/components/extended/__tests__/PrimaryButton.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import PrimaryButton from '../PrimaryButton';
import * as analytics from '@/utils/analytics';
jest.mock('@/utils/analytics');
describe('PrimaryButton', () => {
it('should render children correctly', () => {
render(<PrimaryButton>Click me</PrimaryButton>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('should track analytics on click', () => {
const trackEventSpy = jest.spyOn(analytics, 'trackEvent');
const handleClick = jest.fn();
render(
<PrimaryButton
onClick={handleClick}
trackingLabel="test-button"
>
Test Button
</PrimaryButton>
);
fireEvent.click(screen.getByText('Test Button'));
expect(trackEventSpy).toHaveBeenCalledWith('button_click', {
label: 'test-button',
component: 'PrimaryButton'
});
expect(handleClick).toHaveBeenCalled();
});
it('should apply default size', () => {
const { container } = render(<PrimaryButton>Button</PrimaryButton>);
const button = container.querySelector('button');
expect(button).toHaveClass('btn-large');
});
});
# Mejores Prácticas
# 1. Mantén la Compatibilidad
- Preserva la API original del componente
- Agrega props, no los elimines
- Documenta cambios claramente
# 2. Performance
- Usa React.memo para componentes puros
- Implementa shouldComponentUpdate cuando sea necesario
- Evita re-renders innecesarios
# 3. Documentación
- Documenta todas las props nuevas
- Proporciona ejemplos de uso
- Mantén un changelog
# 4. Testing
- Test unitarios para cada componente extendido
- Tests de integración con componentes Dynamic
- Tests de regresión visual