Padrão DRY para componentes React
Uso este padrão em todos os meus projetos Next.js para manter componentes consistentes e preparados para crescimento. Sempre que crio um novo componente complexo, aplico essa estrutura: centralizo textos no CONTENT, extraio LoadingState/ErrorState/EmptyState como auxiliares, e separo hooks customizados. Isso me economiza tempo significativo quando preciso refatorar ou adicionar funcionalidades, além de deixar tudo pronto para quando decidir implementar i18n.
Prompt abrangente para organizar componentes React seguindo princípios DRY, com foco em reutilização, manutenibilidade e preparação para internacionalização. Ideal para projetos Next.js usando shadcn/ui.
Prompt Completo
Padrão DRY para componentes
Introdução
Este documento descreve o padrão DRY (Don't Repeat Yourself) implementado no projeto para organização de conteúdo em componentes React. Este padrão não apenas melhora a manutenibilidade do código, mas também prepara a base para futuras implementações de internacionalização (i18n) e promove a reutilização de componentes.
Conceito
O padrão consiste em centralizar todo o conteúdo textual e criar componentes auxiliares reutilizáveis, separando completamente a lógica de apresentação do conteúdo e promovendo a composição de componentes.
Benefícios
- ✅ Manutenibilidade: edição centralizada de textos e componentes
- ✅ Consistência: padrão uniforme em todos os componentes
- ✅ Reutilização: componentes auxiliares compartilháveis
- ✅ Preparação para i18n: base estruturada para traduções
- ✅ Legibilidade: separação clara entre dados, lógica e UI
- ✅ Testabilidade: componentes isolados são mais fáceis de testar
- ✅ Performance: menor duplicação de código
- ✅ Type safety: tipagem forte com TypeScript
Estrutura avançada do padrão
Anatomia completa de um componente DRY
1'use client'; 2 3import { /* imports */ } from 'bibliotecas'; 4 5// ========================================== 6// CONTEÚDO E DADOS 7// ========================================== 8 9const CONTENT = { 10 // Estrutura hierárquica do conteúdo 11 title: 'Título principal', 12 description: 'Descrição com possível <strong>HTML</strong>', 13 14 buttons: { 15 primary: 'Botão principal', 16 secondary: 'Botão secundário', 17 save: 'Salvar', 18 cancel: 'Cancelar', 19 }, 20 21 labels: { 22 loading: 'Carregando...', 23 error: 'Erro ao carregar', 24 noData: 'Nenhum dado encontrado', 25 }, 26 27 messages: { 28 success: { 29 created: 'Item criado com sucesso!', 30 updated: 'Item atualizado com sucesso!', 31 }, 32 error: { 33 create: 'Erro ao criar item', 34 update: 'Erro ao atualizar item', 35 }, 36 }, 37 38 dialogs: { 39 delete: { 40 title: 'Confirmar exclusão', 41 description: 'Tem certeza que deseja excluir este item?', 42 }, 43 }, 44} as const; 45 46// Dados estruturados 47const FEATURES = [ 48 { 49 id: 'feature-1', 50 title: 'Feature 1', 51 description: 'Descrição da feature', 52 icon: <Icon className="h-8 w-8" />, 53 config: { 54 enabled: true, 55 priority: 1, 56 }, 57 }, 58 // ... 59] as const; 60 61// Configurações e estilos 62const CONFIG = { 63 animation: { 64 delay: 0.1, 65 duration: 0.6, 66 }, 67 styles: { 68 card: 'rounded-xl bg-white p-6 shadow-lg', 69 button: 'bg-purple-1 text-white hover:bg-purple-2', 70 }, 71} as const; 72 73// ========================================== 74// TIPOS 75// ========================================== 76 77interface ItemData { 78 id: string; 79 name: string; 80 description?: string; 81} 82 83interface FormData { 84 name: string; 85 value: string; 86 type: 'string' | 'number' | 'boolean'; 87} 88 89// ========================================== 90// COMPONENTES AUXILIARES 91// ========================================== 92 93interface LoadingStateProps { 94 message?: string; 95} 96 97const LoadingState = ({ message = CONTENT.labels.loading }: LoadingStateProps) => ( 98 <div className="flex items-center justify-center py-12"> 99 <Loader2 className="h-8 w-8 animate-spin" /> 100 <span className="ml-2">{message}</span> 101 </div> 102); 103 104interface ErrorStateProps { 105 error: any; 106 onRetry: () => void; 107 title?: string; 108} 109 110const ErrorState = ({ error, onRetry, title = CONTENT.labels.error }: ErrorStateProps) => ( 111 <Card> 112 <CardHeader> 113 <CardTitle className="flex items-center gap-2 text-red-600"> 114 <AlertTriangle className="h-5 w-5" /> 115 {title} 116 </CardTitle> 117 </CardHeader> 118 <CardContent> 119 <div className="space-y-4"> 120 <p className="text-muted-foreground"> 121 {error?.message || 'Erro desconhecido'} 122 </p> 123 <Button onClick={onRetry} variant="outline"> 124 {CONTENT.buttons.retry} 125 </Button> 126 </div> 127 </CardContent> 128 </Card> 129); 130 131interface EmptyStateProps { 132 title?: string; 133 description?: string; 134 action?: { 135 label: string; 136 onClick: () => void; 137 }; 138} 139 140const EmptyState = ({ 141 title = CONTENT.labels.noData, 142 description, 143 action 144}: EmptyStateProps) => ( 145 <div className="py-8 text-center text-muted-foreground"> 146 <div className="flex flex-col items-center gap-2"> 147 <Inbox className="h-8 w-8" /> 148 <span>{title}</span> 149 {description && <p className="text-sm">{description}</p>} 150 {action && ( 151 <Button onClick={action.onClick} variant="outline" size="sm" className="mt-2"> 152 {action.label} 153 </Button> 154 )} 155 </div> 156 </div> 157); 158 159interface ItemFormProps { 160 formData: FormData; 161 setFormData: (data: FormData) => void; 162 isEditing?: boolean; 163 errors?: Record<string, string>; 164} 165 166const ItemForm = ({ formData, setFormData, isEditing = false, errors }: ItemFormProps) => { 167 return ( 168 <div className="space-y-4"> 169 <div> 170 <Label htmlFor="name">{CONTENT.labels.name} *</Label> 171 <Input 172 id="name" 173 value={formData.name} 174 onChange={(e) => setFormData({ ...formData, name: e.target.value })} 175 placeholder={CONTENT.placeholders.name} 176 className={errors?.name ? 'border-red-500' : ''} 177 /> 178 {errors?.name && ( 179 <p className="text-sm text-red-500 mt-1">{errors.name}</p> 180 )} 181 </div> 182 183 {/* Outros campos... */} 184 </div> 185 ); 186}; 187 188// ========================================== 189// HOOKS CUSTOMIZADOS 190// ========================================== 191 192const useItemForm = (initialData?: ItemData) => { 193 const [formData, setFormData] = useState<FormData>({ 194 name: initialData?.name || '', 195 value: '', 196 type: 'string', 197 }); 198 199 const [errors, setErrors] = useState<Record<string, string>>({}); 200 201 const validateForm = () => { 202 const newErrors: Record<string, string> = {}; 203 204 if (!formData.name.trim()) { 205 newErrors.name = 'Nome é obrigatório'; 206 } 207 208 setErrors(newErrors); 209 return Object.keys(newErrors).length === 0; 210 }; 211 212 const resetForm = () => { 213 setFormData({ name: '', value: '', type: 'string' }); 214 setErrors({}); 215 }; 216 217 return { 218 formData, 219 setFormData, 220 errors, 221 validateForm, 222 resetForm, 223 }; 224}; 225 226// ========================================== 227// COMPONENTE PRINCIPAL 228// ========================================== 229 230export function MeuComponente() { 231 // Estados e hooks 232 const [isLoading, setIsLoading] = useState(false); 233 const [error, setError] = useState<Error | null>(null); 234 const [items, setItems] = useState<ItemData[]>([]); 235 236 const { formData, setFormData, errors, validateForm, resetForm } = useItemForm(); 237 238 // Handlers organizados 239 const handleCreate = async () => { 240 if (!validateForm()) return; 241 242 try { 243 setIsLoading(true); 244 // Lógica de criação 245 toast.success(CONTENT.messages.success.created); 246 resetForm(); 247 } catch (error) { 248 toast.error(CONTENT.messages.error.create); 249 } finally { 250 setIsLoading(false); 251 } 252 }; 253 254 const handleRetry = () => { 255 // Lógica de retry 256 }; 257 258 // Renderização condicional 259 if (isLoading) { 260 return <LoadingState />; 261 } 262 263 if (error) { 264 return <ErrorState error={error} onRetry={handleRetry} />; 265 } 266 267 return ( 268 <Section> 269 <h1>{CONTENT.title}</h1> 270 <p dangerouslySetInnerHTML={{ __html: CONTENT.description }} /> 271 272 {items.length > 0 ? ( 273 <div className="grid gap-4"> 274 {items.map((item) => ( 275 <ItemCard key={item.id} item={item} /> 276 ))} 277 </div> 278 ) : ( 279 <EmptyState 280 action={{ 281 label: CONTENT.buttons.create, 282 onClick: () => setShowCreateDialog(true) 283 }} 284 /> 285 )} 286 287 {/* Dialogs */} 288 <Dialog 289 open={showCreateDialog} 290 onOpenChange={setShowCreateDialog} 291 title={CONTENT.dialogs.create.title} 292 onConfirm={handleCreate} 293 > 294 <ItemForm 295 formData={formData} 296 setFormData={setFormData} 297 errors={errors} 298 /> 299 </Dialog> 300 </Section> 301 ); 302}
Padrões avançados de organização
1. Estrutura de seções
1// ========================================== 2// CONTEÚDO E DADOS 3// ========================================== 4// Todo conteúdo textual e configurações 5 6// ========================================== 7// TIPOS 8// ========================================== 9// Interfaces e types do componente 10 11// ========================================== 12// COMPONENTES AUXILIARES 13// ========================================== 14// Componentes reutilizáveis específicos 15 16// ========================================== 17// HOOKS CUSTOMIZADOS 18// ========================================== 19// Lógica reutilizável em hooks 20 21// ========================================== 22// COMPONENTE PRINCIPAL 23// ========================================== 24// Componente principal exportado
2. Hierarquia avançada de conteúdo
1const CONTENT = { 2 // Meta informações 3 meta: { 4 title: 'Página principal', 5 description: 'Descrição da página', 6 }, 7 8 // Interface principal 9 ui: { 10 title: 'Título da interface', 11 subtitle: 'Subtítulo da interface', 12 description: 'Descrição da interface', 13 }, 14 15 // Ações e botões 16 actions: { 17 primary: 'Ação principal', 18 secondary: 'Ação secundária', 19 destructive: 'Deletar', 20 }, 21 22 // Labels de formulários 23 form: { 24 labels: { 25 name: 'Nome', 26 email: 'E-mail', 27 password: 'Senha', 28 }, 29 placeholders: { 30 name: 'Digite seu nome', 31 email: 'seu@email.com', 32 }, 33 validation: { 34 required: 'Campo obrigatório', 35 email: 'E-mail inválido', 36 }, 37 }, 38 39 // Estados da aplicação 40 states: { 41 loading: 'Carregando...', 42 empty: 'Nenhum item encontrado', 43 error: 'Erro ao carregar dados', 44 }, 45 46 // Mensagens de feedback 47 feedback: { 48 success: { 49 save: 'Salvo com sucesso!', 50 delete: 'Deletado com sucesso!', 51 }, 52 error: { 53 save: 'Erro ao salvar', 54 delete: 'Erro ao deletar', 55 network: 'Erro de conexão', 56 }, 57 }, 58 59 // Diálogos e modais 60 dialogs: { 61 confirm: { 62 title: 'Confirmar ação', 63 description: 'Tem certeza?', 64 }, 65 delete: { 66 title: 'Deletar item', 67 description: 'Esta ação não pode ser desfeita', 68 }, 69 }, 70} as const;
3. Componentes auxiliares tipados
1// ========================================== 2// COMPONENTES AUXILIARES 3// ========================================== 4 5// Estado de carregamento reutilizável 6interface LoadingStateProps { 7 message?: string; 8 size?: 'sm' | 'md' | 'lg'; 9 showSpinner?: boolean; 10} 11 12const LoadingState = ({ 13 message = CONTENT.states.loading, 14 size = 'md', 15 showSpinner = true 16}: LoadingStateProps) => { 17 const sizeClasses = { 18 sm: 'h-4 w-4', 19 md: 'h-8 w-8', 20 lg: 'h-12 w-12' 21 }; 22 23 return ( 24 <div className="flex items-center justify-center py-12"> 25 {showSpinner && <Loader2 className={`animate-spin ${sizeClasses[size]}`} />} 26 <span className="ml-2">{message}</span> 27 </div> 28 ); 29}; 30 31// Estado de erro com retry 32interface ErrorStateProps { 33 error: Error | string; 34 onRetry?: () => void; 35 title?: string; 36 showRetry?: boolean; 37} 38 39const ErrorState = ({ 40 error, 41 onRetry, 42 title = CONTENT.states.error, 43 showRetry = true 44}: ErrorStateProps) => { 45 const errorMessage = typeof error === 'string' ? error : error.message; 46 47 return ( 48 <Card className="border-red-200"> 49 <CardHeader> 50 <CardTitle className="flex items-center gap-2 text-red-600"> 51 <AlertTriangle className="h-5 w-5" /> 52 {title} 53 </CardTitle> 54 </CardHeader> 55 <CardContent> 56 <div className="space-y-4"> 57 <p className="text-muted-foreground">{errorMessage}</p> 58 {showRetry && onRetry && ( 59 <Button onClick={onRetry} variant="outline" size="sm"> 60 {CONTENT.actions.retry} 61 </Button> 62 )} 63 </div> 64 </CardContent> 65 </Card> 66 ); 67}; 68 69// Estado vazio customizável 70interface EmptyStateProps { 71 icon?: React.ComponentType<{ className?: string }>; 72 title?: string; 73 description?: string; 74 action?: { 75 label: string; 76 onClick: () => void; 77 variant?: 'default' | 'outline'; 78 }; 79} 80 81const EmptyState = ({ 82 icon: Icon = Inbox, 83 title = CONTENT.states.empty, 84 description, 85 action 86}: EmptyStateProps) => ( 87 <div className="py-12 text-center"> 88 <div className="flex flex-col items-center gap-4"> 89 <Icon className="h-12 w-12 text-muted-foreground" /> 90 <div className="space-y-2"> 91 <h3 className="text-lg font-medium">{title}</h3> 92 {description && ( 93 <p className="text-muted-foreground max-w-md">{description}</p> 94 )} 95 </div> 96 {action && ( 97 <Button 98 onClick={action.onClick} 99 variant={action.variant || 'default'} 100 size="sm" 101 > 102 {action.label} 103 </Button> 104 )} 105 </div> 106 </div> 107); 108 109// Formulário reutilizável 110interface FormFieldProps { 111 label: string; 112 error?: string; 113 required?: boolean; 114 children: React.ReactNode; 115} 116 117const FormField = ({ label, error, required, children }: FormFieldProps) => ( 118 <div className="space-y-2"> 119 <Label className="text-sm font-medium"> 120 {label} 121 {required && <span className="text-red-500 ml-1">*</span>} 122 </Label> 123 {children} 124 {error && ( 125 <p className="text-sm text-red-500">{error}</p> 126 )} 127 </div> 128);
4. Hooks customizados para lógica reutilizável
1// ========================================== 2// HOOKS CUSTOMIZADOS 3// ========================================== 4 5// Hook para gerenciamento de formulários 6const useFormValidation = <T extends Record<string, any>>( 7 initialData: T, 8 validationRules: Record<keyof T, (value: any) => string | undefined> 9) => { 10 const [formData, setFormData] = useState<T>(initialData); 11 const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}); 12 const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({}); 13 14 const validateField = (field: keyof T, value: any) => { 15 const rule = validationRules[field]; 16 return rule ? rule(value) : undefined; 17 }; 18 19 const validateForm = () => { 20 const newErrors: Partial<Record<keyof T, string>> = {}; 21 22 Object.keys(validationRules).forEach((field) => { 23 const error = validateField(field as keyof T, formData[field]); 24 if (error) { 25 newErrors[field as keyof T] = error; 26 } 27 }); 28 29 setErrors(newErrors); 30 return Object.keys(newErrors).length === 0; 31 }; 32 33 const updateField = (field: keyof T, value: any) => { 34 setFormData(prev => ({ ...prev, [field]: value })); 35 setTouched(prev => ({ ...prev, [field]: true })); 36 37 // Validação em tempo real se o campo já foi tocado 38 if (touched[field]) { 39 const error = validateField(field, value); 40 setErrors(prev => ({ ...prev, [field]: error })); 41 } 42 }; 43 44 const resetForm = () => { 45 setFormData(initialData); 46 setErrors({}); 47 setTouched({}); 48 }; 49 50 return { 51 formData, 52 setFormData, 53 errors, 54 touched, 55 updateField, 56 validateForm, 57 resetForm, 58 isValid: Object.keys(errors).length === 0, 59 }; 60}; 61 62// Hook para estados assíncronos 63const useAsyncState = <T>(asyncFn: () => Promise<T>) => { 64 const [data, setData] = useState<T | null>(null); 65 const [loading, setLoading] = useState(false); 66 const [error, setError] = useState<Error | null>(null); 67 68 const execute = async () => { 69 try { 70 setLoading(true); 71 setError(null); 72 const result = await asyncFn(); 73 setData(result); 74 return result; 75 } catch (err) { 76 setError(err as Error); 77 throw err; 78 } finally { 79 setLoading(false); 80 } 81 }; 82 83 return { data, loading, error, execute, retry: execute }; 84};
Patterns de reutilização
1. Componentes de estado condicionais
1// Wrapper para renderização condicional baseada em estado 2interface ConditionalRenderProps<T> { 3 data: T[] | null; 4 loading: boolean; 5 error: Error | null; 6 onRetry?: () => void; 7 emptyComponent?: React.ComponentType; 8 loadingComponent?: React.ComponentType; 9 errorComponent?: React.ComponentType<{ error: Error; onRetry?: () => void }>; 10 children: (data: T[]) => React.ReactNode; 11} 12 13const ConditionalRender = <T,>({ 14 data, 15 loading, 16 error, 17 onRetry, 18 emptyComponent: EmptyComponent = EmptyState, 19 loadingComponent: LoadingComponent = LoadingState, 20 errorComponent: ErrorComponent = ErrorState, 21 children, 22}: ConditionalRenderProps<T>) => { 23 if (loading) return <LoadingComponent />; 24 if (error) return <ErrorComponent error={error} onRetry={onRetry} />; 25 if (!data || data.length === 0) return <EmptyComponent />; 26 27 return <>{children(data)}</>; 28}; 29 30// Uso 31<ConditionalRender 32 data={items} 33 loading={isLoading} 34 error={error} 35 onRetry={handleRetry} 36> 37 {(items) => ( 38 <div className="grid gap-4"> 39 {items.map(item => <ItemCard key={item.id} item={item} />)} 40 </div> 41 )} 42</ConditionalRender>
2. Factory de diálogos
1// Factory para criar diálogos com padrões consistentes 2const createDialog = (config: { 3 title: string; 4 description?: string; 5 intent?: 'default' | 'warning' | 'danger'; 6}) => { 7 return ({ 8 open, 9 onOpenChange, 10 onConfirm, 11 onCancel, 12 children, 13 ...props 14 }: DialogProps) => ( 15 <Dialog 16 open={open} 17 onOpenChange={onOpenChange} 18 title={config.title} 19 description={config.description} 20 intent={config.intent} 21 onConfirm={onConfirm} 22 onCancel={onCancel} 23 {...props} 24 > 25 {children} 26 </Dialog> 27 ); 28}; 29 30// Criar diálogos específicos 31const DeleteConfirmDialog = createDialog({ 32 title: CONTENT.dialogs.delete.title, 33 description: CONTENT.dialogs.delete.description, 34 intent: 'danger', 35}); 36 37const SaveChangesDialog = createDialog({ 38 title: CONTENT.dialogs.save.title, 39 description: CONTENT.dialogs.save.description, 40 intent: 'warning', 41});
Preparação avançada para i18n
Estrutura de namespaces
1// Atual (preparação) 2const CONTENT = { 3 pages: { 4 dashboard: { 5 title: 'Dashboard', 6 welcome: 'Bem-vindo de volta!', 7 }, 8 settings: { 9 title: 'Configurações', 10 subtitle: 'Gerencie suas preferências', 11 }, 12 }, 13 14 common: { 15 buttons: { 16 save: 'Salvar', 17 cancel: 'Cancelar', 18 delete: 'Deletar', 19 }, 20 states: { 21 loading: 'Carregando...', 22 empty: 'Nenhum item encontrado', 23 }, 24 }, 25} as const; 26 27// Futuro (com i18n) 28const useContent = () => { 29 const t = useTranslations(); 30 31 return { 32 pages: { 33 dashboard: { 34 title: t('pages.dashboard.title'), 35 welcome: t('pages.dashboard.welcome'), 36 }, 37 settings: { 38 title: t('pages.settings.title'), 39 subtitle: t('pages.settings.subtitle'), 40 }, 41 }, 42 43 common: { 44 buttons: { 45 save: t('common.buttons.save'), 46 cancel: t('common.buttons.cancel'), 47 delete: t('common.buttons.delete'), 48 }, 49 states: { 50 loading: t('common.states.loading'), 51 empty: t('common.states.empty'), 52 }, 53 }, 54 }; 55};
Hook de conteúdo reutilizável
1// Hook para centralizar acesso ao conteúdo 2const useComponentContent = (componentName: string) => { 3 // Atualmente retorna constantes locais 4 // No futuro usará traduções 5 6 const getContent = () => { 7 switch (componentName) { 8 case 'ConfigsManagement': 9 return CONFIGS_CONTENT; 10 case 'UserManagement': 11 return USERS_CONTENT; 12 default: 13 return COMMON_CONTENT; 14 } 15 }; 16 17 return getContent(); 18}; 19 20// Uso no componente 21export function ConfigsManagement() { 22 const content = useComponentContent('ConfigsManagement'); 23 24 return ( 25 <div> 26 <h1>{content.title}</h1> 27 {/* ... */} 28 </div> 29 ); 30}
Boas práticas avançadas
✅ Padrões recomendados
-
Separação clara de responsabilidades
1// ❌ Misturado 2const MyComponent = () => { 3 const [data, setData] = useState([]); 4 5 return ( 6 <div> 7 <h1>Lista de usuários</h1> 8 {data.length > 0 ? ( 9 data.map(user => <div key={user.id}>{user.name}</div>) 10 ) : ( 11 <div>Nenhum usuário encontrado</div> 12 )} 13 </div> 14 ); 15}; 16 17// ✅ Separado 18const CONTENT = { 19 title: 'Lista de usuários', 20 emptyMessage: 'Nenhum usuário encontrado' 21}; 22 23const UserList = ({ users }) => ( 24 <div> 25 {users.map(user => <UserCard key={user.id} user={user} />)} 26 </div> 27); 28 29const EmptyUsers = () => ( 30 <EmptyState title={CONTENT.emptyMessage} /> 31); 32 33const MyComponent = () => { 34 const [data, setData] = useState([]); 35 36 return ( 37 <div> 38 <h1>{CONTENT.title}</h1> 39 {data.length > 0 ? <UserList users={data} /> : <EmptyUsers />} 40 </div> 41 ); 42};
-
Componentes auxiliares parametrizáveis
1// ✅ Flexível e reutilizável 2interface ActionButtonsProps { 3 actions: Array<{ 4 label: string; 5 onClick: () => void; 6 variant?: 'default' | 'outline' | 'destructive'; 7 icon?: React.ComponentType; 8 }>; 9 loading?: boolean; 10 disabled?: boolean; 11} 12 13const ActionButtons = ({ actions, loading, disabled }: ActionButtonsProps) => ( 14 <div className="flex gap-2"> 15 {actions.map((action, index) => ( 16 <Button 17 key={index} 18 onClick={action.onClick} 19 variant={action.variant} 20 disabled={disabled || loading} 21 className="flex items-center gap-2" 22 > 23 {action.icon && <action.icon className="h-4 w-4" />} 24 {action.label} 25 </Button> 26 ))} 27 </div> 28);
-
Tipagem forte com template literals
1// ✅ Type safety para chaves de conteúdo 2type ContentKeys = 3 | 'title' 4 | 'description' 5 | `buttons.${string}` 6 | `messages.success.${string}` 7 | `messages.error.${string}`; 8 9const getContentValue = (key: ContentKeys): string => { 10 // Implementação type-safe 11};
-
Composition pattern
1// ✅ Componentes compostos 2const ConfigsTable = { 3 Root: ({ children }: { children: React.ReactNode }) => ( 4 <div className="rounded-md border">{children}</div> 5 ), 6 7 Header: ({ columns }: { columns: string[] }) => ( 8 <TableHeader> 9 <TableRow> 10 {columns.map(col => <TableHead key={col}>{col}</TableHead>)} 11 </TableRow> 12 </TableHeader> 13 ), 14 15 Body: ({ children }: { children: React.ReactNode }) => ( 16 <TableBody>{children}</TableBody> 17 ), 18 19 Row: ({ children }: { children: React.ReactNode }) => ( 20 <TableRow>{children}</TableRow> 21 ), 22}; 23 24// Uso 25<ConfigsTable.Root> 26 <ConfigsTable.Header columns={['Nome', 'Valor', 'Ações']} /> 27 <ConfigsTable.Body> 28 {configs.map(config => ( 29 <ConfigsTable.Row key={config.id}> 30 <TableCell>{config.name}</TableCell> 31 <TableCell>{config.value}</TableCell> 32 <TableCell> 33 <ActionButtons actions={getConfigActions(config)} /> 34 </TableCell> 35 </ConfigsTable.Row> 36 ))} 37 </ConfigsTable.Body> 38</ConfigsTable.Root>
❌ Anti-padrões a evitar
- Componentes auxiliares muito específicos
- Constantes de conteúdo muito profundas (mais de 4 níveis)
- Misturar lógica de negócio em componentes auxiliares
- Criar hooks customizados para lógica muito simples
- Repetir estruturas similares sem abstrair
Migração incremental
Estratégia de implementação
- Fase 1: aplicar DRY básico (conteúdo centralizado)
- Fase 2: extrair componentes auxiliares
- Fase 3: criar hooks customizados
- Fase 4: implementar composition patterns
- Fase 5: preparar para i18n
Checklist de verificação
- [ ] Todo conteúdo textual está em constantes
CONTENT
- [ ] Componentes auxiliares estão fora do return principal
- [ ] Estados condicionais têm componentes dedicados
- [ ] Formulários são reutilizáveis
- [ ] Hooks customizados isolam lógica complexa
- [ ] Tipagem forte com TypeScript
- [ ] Preparação adequada para i18n
Ferramentas e validação
Scripts de validação
1# Verificar strings hardcoded 2grep -r "className.*>[A-Z]" app/components/ --exclude-dir=node_modules 3 4# Verificar uso do padrão DRY 5grep -r "const CONTENT" app/components/ | wc -l 6 7# Verificar componentes auxiliares 8grep -r "// COMPONENTES AUXILIARES" app/components/ | wc -l
Métricas de qualidade
- Reusabilidade: % de componentes que são reutilizados
- Manutenibilidade: tempo para alterar textos/estilos
- [ ] Consistência: uso uniforme de padrões
- [ ] Preparação i18n: % de strings externalizadas
Conclusão
O padrão DRY avançado implementado oferece uma arquitetura robusta e escalável para componentes React, promovendo reutilização, manutenibilidade e preparação para internacionalização. A separação em componentes auxiliares e hooks customizados garante código limpo e testável, enquanto mantém a flexibilidade necessária para diferentes contextos de uso.
Curtiu este prompt?
Explore mais prompts testados e refinados para uso profissional.
Ver todos os prompts