Introdução
Você já criou um componente de Botão que começou simples, recebendo apenas um titlee um onPress, e meses depois ele tinha mais de 20 propsopcionais como iconLeft, iconRight, loadingColor, textStyle, containerStyle?
Recentemente, retomando meus estudos no ecossistema React Native, revisitei um padrão que resolve o problema da rigidez em components: os Compound Components (Componentes Compostos), e que se aplica tanto a React Native quanto React.
Neste artigo, vou mostrar como transformar um botão rígido e difícil de manter em uma peça de LEGO flexível e reutilizável.
O Jeito "Ingênuo"
Quando começamos, a tendência natural é centralizar toda a lógica em um único lugar. Vamos imaginar um botão clássico que pode ter um ícone e um estado de carregamento.
A Implementação Rígida:
import {
TouchableOpacity,
Text,
ActivityIndicator,
TouchableOpacityProps,
TextStyle
} from "react-native";
import { MaterialIcons } from "@expo/vector-icons";
import { styles } from "./styles";
type BadButtonProps = TouchableOpacityProps & {
title: string;
isLoading?: boolean;
iconName?: keyof typeof MaterialIcons.glyphMap;
iconPosition?: "left" | "right";
titleStyle?: TextStyle;
};
export function BadButton({
title,
isLoading,
iconName,
iconPosition = "left",
titleStyle,
}: BadButtonProps) {
return (
<TouchableOpacity style={styles.container} {...rest}>
{isLoading ? (
<ActivityIndicator color="#FFF" />
) : (
<>
{iconName && iconPosition === "left" && (
<MaterialIcons
name={iconName}
size={24}
color="#FFF"
style={styles.iconLeft}
/>
)}
<Text style={[styles.title, titleStyle]}>
{title}
</Text>
{iconName && iconPosition === "right" && (
<MaterialIcons
name={iconName}
size={24}
color="#FFF"
style={styles.iconRight}
/>
)}
</>
)}
</TouchableOpacity>
);
}
O Uso Rígido
Para usar esse componente, você fica refém das propsque o dev definiu. Se você quiser colocar dois ícones? Ou um ícone em cima e o texto embaixo? Você teria que alterar o componente original.
// index.tsx
<BadButton
title="Entrar"
onPress={() => console.log("Click")}
iconName="login"
iconPosition="right" // E se eu quisesse no topo? Teria que editar o componente e criar mais lógicas.
isLoading={loading}
/>
A Solução Elegante: Compound Components
A ideia do padrão Compound Components é a Decomposição. Em vez de um componente tentar fazer tudo, ele orquestra componentes menores que trabalham juntos.
O componente pai (Button) gerencia o contexto (como o isLoadinge o containertocável), enquanto os filhos (Title, Icon) cuidam apenas de se renderizar.
A Implementação Flexível:
import React from "react";
import {
TouchableOpacity,
TouchableOpacityProps,
Text,
TextProps,
ActivityIndicator
} from "react-native";
import { MaterialIcons } from "@expo/vector-icons";
import { styles } from "./styles";
// 1. Tipagem e Componente Raiz (Root)
type ButtonProps = TouchableOpacityProps & {
isLoading?: boolean;
};
function Button({ children, style, isLoading = false, ...rest }: ButtonProps) {
return (
<TouchableOpacity
style={[styles.container, style]}
activeOpacity={0.8}
disabled={isLoading}
{...rest}
>
{isLoading ? (
<ActivityIndicator size="small" color="#FFF" />
) : (
// Aqui está a mágica: o children pode ser qualquer coisa, em qualquer ordem!
children
)}
</TouchableOpacity>
);
}
// 2. Componentes Filhos (Partes do LEGO)
function Title({ children, style, ...rest }: TextProps) {
return <Text style={[styles.title, style]} {...rest}>{children}</Text>;
}
type IconProps = {
icon: keyof typeof MaterialIcons.glyphMap;
};
function Icon({ icon }: IconProps) {
return <MaterialIcons name={icon} size={24} color="#FFF" />;
}
// 3. Associação: Exportando como propriedades do Pai
Button.Title = Title;
Button.Icon = Icon;
export { Button };
O uso Flexível:
Agora, veja como o uso no seu App.tsx ou index.tsx se torna expressivo. Você não passa configurações, você compõe a interface.
Cenário 1: Botão Simples
<Button onPress={() => router.navigate("/home")}>
<Button.Title>Começar</Button.Title>
</Button>
Cenário 2: Ícone na Esquerda
<Button onPress={handleLogin}>
<Button.Icon icon="login" />
<Button.Title>Entrar</Button.Title>
</Button>
Cenário 3: Personalização Total (Ex: Botão Vertical)
Como o componente Buttonaceita style (e concatena com o styles.container), podemos mudar o flex-direction sem quebrar a lógica interna.
<Button style={{ flexDirection: 'column', height: 100 }}>
<Button.Icon icon="cloud-upload" />
<Button.Title>Upload</Button.Title>
</Button>
Por que adotar esse padrão?
Separação de Responsabilidades: O Buttonnão precisa saber qual ícone será renderizado, nem onde. Ele só precisa saber ser um botão clicável.
Manutenibilidade: Se você precisar mudar a biblioteca de ícones amanhã, você só mexe no componente Icon. O resto do código permanece intacto.
Legibilidade: Ler <Button.Icon /> <Button.Title /> é muito mais intuitivo do que ler iconPosition="left".
Extensibilidade: Quer adicionar um Badgede notificação no botão? Basta criar Button.Badge e começar a usar, sem quebrar os botões existentes.
Conclusão
A experiência e estudos em desenvolvimento de software muitas vezes não nos faz escrever códigos complexos, mas em simplificar o complexo. O padrão de Compound Components é um excelente exemplo de como aplicar princípios de SOLID (especificamente o Princípio da Responsabilidade Única bem como o Princípio Aberto/Fechado), resultando em códigos mais flexíveis, reutilizáveis e fáceis de manter no front-end.
Ao decompor seus componentes, você ganha flexibilidade e poupa horas de refatoração no futuro.
Gostou da dica? Já conhecia esse padrão? Deixe nos comentários como você lida com componentes complexos no seu projeto!

Top comments (0)