DEV Community

Cover image for O Poder dos Compound Components no React e React Native
Diogo Mascarenhas
Diogo Mascarenhas

Posted on

O Poder dos Compound Components no React e React Native

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Cenário 2: Ícone na Esquerda

<Button onPress={handleLogin}>
  <Button.Icon icon="login" />
  <Button.Title>Entrar</Button.Title>
</Button>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)