DEV Community

Cover image for React e SOLID: A Componentização Perfeita no Text Decoder Challenge
Vânia Gomes
Vânia Gomes

Posted on • Edited on

React e SOLID: A Componentização Perfeita no Text Decoder Challenge

Introdução

A componentização é um dos pilares fundamentais do React. Ela permite dividir a interface em partes menores, independentes e reutilizáveis, facilitando a manutenção, a escalabilidade e a legibilidade do código. No projeto Text Decoder Challenge, utilizamos essa abordagem para organizar a interface de codificação e decodificação de textos de maneira eficiente. Além disso, aplicamos intencionalmente alguns dos princípios SOLID, que, quando combinados com a componentização, resultam em um código mais modular, flexível, facilidade para realizar os teste e fácil de manter.

O Que é Componentização?

Componentização é o processo de dividir a interface do usuário em componentes. Cada componente encapsula uma funcionalidade específica, tornando o código mais modular. No React, os componentes funcionam como funções JavaScript, que aceitam "props" (propriedades) e retornam elementos que descrevem o que deve ser renderizado na tela.

No Text Decoder Challenge, cada parte da interface – o campo de texto, os botões de codificar e decodificar, e o campo de exibição dos resultados – é um componente separado. Isso torna o código mais fácil de gerenciar e possibilita a reutilização desses componentes em diferentes contextos.

Propriedades (Props) e Estado (State)

As props e o useState têm uma relação direta com a componentização no React, pois são ferramentas essenciais para a criação e funcionamento dos componentes.

Relação das props com a Componentização

  • Props (propriedades) permitem que os componentes se comuniquem entre si. Em um projeto componentizado, como o Text Decoder Challenge, diferentes componentes podem ter suas responsabilidades individuais (como um Button ou Textarea), mas eles precisam trocar informações para funcionar de maneira coesa.

  • As props são a forma de passar dados de um componente pai para um componente filho. Isso é fundamental para manter a modularidade, pois os componentes filhos são reutilizáveis e independentes, mas ainda podem exibir ou agir com base nas informações recebidas do componente pai.

  • Exemplo no Text Decoder Challenge: O componente Textarea recebe o valor do texto via props do componente pai (como o Decoder ou App), o que permite que o mesmo componente de entrada seja reutilizado em diferentes contextos, exibindo valores diferentes.

export function Textarea({ value, onChange, placeholder, className, ...props }) {
  return (
    <textarea
      value={value}
      onChange={onChange}
      placeholder={placeholder}
      className={className}
      {...props}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Uso do Textarea no componente App

<section className={styles.container_text}>
          <Textarea
            id="inputApp"
            value={inputValue}
            onChange={handleInputChange}
            placeholder="Digite seu texto"
            className={styles.container__text__input}
          />
</section>
Enter fullscreen mode Exit fullscreen mode

Relação do useState com a Componentização

  • O useState é o hook que gerencia o estado interno de um componente. Ele é crucial para permitir que os componentes tenham seu próprio comportamento dinâmico, independentemente do restante da aplicação. Cada componente pode ter seu próprio estado (como o valor do texto inserido no Textarea ou o feedback de cópia bem-sucedida no Decoder), permitindo que reaja às interações do usuário de forma isolada.
  • A componentização se beneficia diretamente da utilização do useState, já que o estado gerenciado dentro de cada componente é encapsulado, o que garante que o comportamento seja específico daquele componente sem interferir em outros.
  • Exemplo no Text Decoder Challenge: O componente Decoder utiliza o useState para controlar o valor da entrada de texto e o sucesso da cópia. Isso mantém a lógica local, dentro do componente, promovendo a separação de responsabilidades e encapsulamento.

Hook useCopyMessage

import { useState } from "react";

export function useCopyMessage() {
  const [copySuccess, setCopySuccess] = useState('');

  function showMessage(message, duration = 3000) {
    setCopySuccess(message);
    setTimeout(() => {
      setCopySuccess('');
    }, duration);
  }

  return { copySuccess, showMessage };
}
Enter fullscreen mode Exit fullscreen mode

Utilitário copyUtil

export function copyTextToClipboard(text, showMessage) {
  navigator.clipboard.writeText(text)
    .then(() => {
      showMessage('Texto copiado com sucesso!');
    })
    .catch(() => {
      showMessage('Ops, ocorreu um erro ao copiar o texto!');
    });
}
Enter fullscreen mode Exit fullscreen mode

Uso do hook e do utilitário no componente Decoder

export function Decoder({ outputValue }) {
  const { copySuccess, showMessage } = useCopyMessage();

  function handleCopyText() {
    copyTextToClipboard(outputValue, showMessage);
  }

    rendering UI 

 {copySuccess && ( <p className={styles.copyMessage}>{copySuccess}</p> )}
}
Enter fullscreen mode Exit fullscreen mode

Exemplo Prático: Desafio do Decodificador de Texto

O Text Decoder Challenge permite que o usuário insira um texto e escolha entre codificar ou decodificar a mensagem. O projeto também fornece a opção de copiar o texto que deseja realizar a criptografia ou descriptografia para a área em que será realizada a codificação ou decodificação. A seguir, a estrutura básica do projeto, com componentes para cada parte essencial da interface:

Aplicando a Componentização

A estrutura do projeto já foi organizada de forma a utilizar componentes. Aqui estão os principais componentes identificados:

Button: Um componente reutilizável para criar diferentes botões (por exemplo, "Codificar", "Decodificar", "Copiar").

No início, o componente Button exigia que eu criasse novas classes CSS para cada estilo de botão diferente. Além disso, para mover o botão "Copiar" um pouco mais para cima quando ele fosse clicado, eu precisava realizar verificações manuais, o que resultava em duplicação de código e manutenção trabalhosa, pois as alterações precisavam ser feitas em vários locais da aplicação. Isso também gerava inconsistência visual, já que o estilo dos botões não era centralizado.

Com a introdução da prop classNameVariant, o componente Button tornou-se escalável. Agora, novos tipos de botões e estilos podem ser adicionados simplesmente passando um novo valor para a prop classNameVariant, sem a necessidade de modificar o código base do componente. Isso permitiu aplicar o Princípio Aberto/Fechado (OCP), já que o componente está aberto para extensão, mas fechado para modificação. Dessa forma, a manutenção ficou mais simples, e a consistência visual foi garantida em toda a aplicação.

Além disso, o componente Button segue o Princípio da Responsabilidade Única (SRP), uma vez que ele é responsável apenas por renderizar o botão e lidar com o evento de clique. Toda a lógica relacionada ao comportamento do clique é delegada externamente por meio da prop onClick, o que garante que o Button permaneça simples e focado em sua única responsabilidade. Isso evita que o componente fique sobrecarregado com várias responsabilidades, tornando o código mais claro e de fácil manutenção.

export function Button({ label, onClick, type, classNameVariant, className }) {
  return (
    <button
      type={type}
      onClick={onClick}
      className={`${className} ${classNameVariant} ${className}`}
    >
      {label}
    </button>
  );
}

Button.propTypes = {
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func.isRequired,
  type: PropTypes.oneOf(['button', 'submit', 'reset']),
  classNameVariant: PropTypes.oneOf(['btn__encrypt', 'btn__decrypt', 'btn__copy']),
  className: PropTypes.string,
};

Button.defaultProps = {
  type: 'button',
  classNameVariant: '',
  className: '',
};
Enter fullscreen mode Exit fullscreen mode

Decoder: está apenas focado em renderizar e gerenciar a UI.

O componente Decoder está seguindo o SRP ao manter a responsabilidade principal focada na renderização da interface para decodificar e copiar texto. Ele não está diretamente lidando com a lógica de cópia ou manipulação de estado; essas funcionalidades foram separadas em outros módulos:

  • A lógica de exibir a mensagem de cópia foi delegada para o hook useCopyMessage.
  • A função de copiar o texto para a área de transferência foi movida para o utilitário copyTextToClipboard.

O DIP foi aplicado ao separar a lógica de cópia e manipulação de mensagens em módulos externos (useCopyMessage e copyTextToClipboard). O componente Decoder depende de abstrações (no caso, um hook e um utilitário) em vez de lidar com a implementação concreta diretamente no componente.

Essa abordagem mantém o componente Decoder desacoplado da lógica específica, tornando-o mais fácil de testar e modificar. Se a implementação de como copiar o texto para a área de transferência mudar, ou se a forma de exibir a mensagem de sucesso for alterada, o componente Decoder não precisará ser modificado, já que ele apenas consome essas funcionalidades através de dependências injetadas.

Hook useCopyMessage

import { useState } from "react";

export function useCopyMessage() {
  const [copySuccess, setCopySuccess] = useState('');
  function showMessage(message, duration = 3000) {
    setCopySuccess(message);
    setTimeout(() => {
      setCopySuccess('');
    }, duration);
  }
  return { copySuccess, showMessage };
}
Enter fullscreen mode Exit fullscreen mode

Utilitário copyTextToClipboard

export function copyTextToClipboard(text, showMessage) {
  navigator.clipboard.writeText(text)
    .then(() => {
      showMessage('Texto copiado com sucesso!');
    })
    .catch(() => {
      showMessage('Ops, ocorreu um erro ao copiar o texto!');
    });
}
Enter fullscreen mode Exit fullscreen mode

Componente Decoder

export function Decoder({ outputValue }) {
  const { copySuccess, showMessage } = useCopyMessage();

  function handleCopyText() { 
   copyTextToClipboard(outputValue, showMessage);
  }

   return ( 
          rendering UI 
   );
}
Enter fullscreen mode Exit fullscreen mode

Textarea: Campo de entrada de texto, que pode ser reutilizado em diferentes partes do projeto.

O Princípio da Responsabilidade Única (SRP) foi aplicado no componente Textarea, garantindo que sua única função seja renderizar um campo de texto e propagar mudanças através do evento onChange. O componente não lida com lógica adicional, como validação ou manipulação de dados; toda a manipulação é feita externamente, e ele apenas se preocupa com a renderização do elemento <textarea> baseado nas props fornecidas, como value, onChange e placeholder. Além disso, o Princípio Aberto/Fechado (OCP) é aplicado, uma vez que o componente está aberto para extensão através das props adicionais (...props) e fechado para modificação. Isso significa que novas funcionalidades ou atributos podem ser adicionados ao <textarea> sem modificar o código original do componente, graças ao uso do spread operator, que permite passar outras props HTML, como disabled ou maxlength.

Componente Textarea

export function Textarea({ value, onChange, placeholder, className, ...props }) {
  return (
    <textarea
      value={value}
      onChange={onChange}
      placeholder={placeholder}
      className={className}
      {...props}
    />
  );
}
};
Enter fullscreen mode Exit fullscreen mode

App: O Princípio da Responsabilidade Única (SRP) é aplicado ao componente App para realizar apenas uma tarefa específica,tanto internamente quanto externamente.

Ele gerencia o estado e a lógica principal, delegando a renderização e funções específicas para componentes como Header, Textarea, Button e Decoder. Cada um desses componentes tem uma responsabilidade clara e única, seja exibir o cabeçalho, capturar a entrada de texto, disparar uma ação com botões ou exibir o resultado.

Funções específicas de validação
A função validationTextareField é responsável exclusivamente por validar a entrada de texto, tanto para criptografia quanto para descriptografia. Isso isola a lógica de validação em uma função separada, facilitando ajustes futuros.

function validationTextareField(inputValue, isDecrypt = false) {
  if (!inputValue.trim()) {
    setErrorMessage('Campo de entrada está vazio!');
    return false;
  }

  if (isDecrypt) {
    if (!/^[a-z!]+$/.test(inputValue)) {
      setErrorMessage('Texto criptografado inválido!');
      return false;
    }
  } else {
    if (!/^[a-z ]+$/.test(inputValue)) {
      setErrorMessage('Texto para decodificação inválido!');
      return false;
    }
  }

  return true;
}
Enter fullscreen mode Exit fullscreen mode

Funções de Utilidades
As funções encryptText e decryptText são separadas em um módulo de utilidades (decodeUtil), dedicadas exclusivamente ao processo de criptografia e descriptografia do texto. Isso mantém o foco dessas funções em uma única tarefa específica, sem misturar responsabilidades.

export const encryptText = (input) => {
    return input.split('').map(char => String.fromCharCode(char.charCodeAt(0) + 1)).join('');
  };

  export const decryptText = (input) => {
    return input.split('').map(char => String.fromCharCode(char.charCodeAt(0) - 1)).join('');
  };
Enter fullscreen mode Exit fullscreen mode

Código de Exemplo do Componente App

// Funções de utilidades para criptografia e descriptografia
export const encryptText = (input) => {
  return input.split('').map(char => String.fromCharCode(char.charCodeAt(0) + 1)).join('');
};

export const decryptText = (input) => {
  return input.split('').map(char => String.fromCharCode(char.charCodeAt(0) - 1)).join('');
};

import React, { useState } from "react";
import { encryptText, decryptText } from "./utils/decodeUtil";

import { Button } from "./components/Button/Button";
import { Decoder } from "./components/Decoder/Decoder";
import { Header } from "./components/Header/Header";
import { Textarea } from "./components/Textarea/Textarea";

import bi_exclamation from "./assets/bi_exclamation-circle-fill.png";

import styles from "./App.module.css";
import "./styles/global.css";

function App() {
  const [inputValue, setInputValue] = useState('');
  const [outputValue, setOutputValue] = useState('');
  const [errorMessage, setErrorMessage] = useState('');

  // Função de validação ajustada
  function validationTextareField(inputValue, isDecrypt = false) {
    if (!inputValue.trim()) {
      setErrorMessage('Campo de entrada está vazio!');
      return false;
    }

    if (isDecrypt) {
      if (!/^[a-z!]+$/.test(inputValue)) {
        setErrorMessage('Texto criptografado inválido!');
        return false;
      }
    } else {
      if (!/^[a-z ]+$/.test(inputValue)) {
        setErrorMessage('Texto para decodificação inválido!');
        return false;
      }
    }
    return true;
  }

  function handleEncrypt() {
    if (!validationTextareField(inputValue)) return;
    const encrypted = encryptText(inputValue);
    setOutputValue(encrypted);
    setErrorMessage('');
  }

  function handleDecrypt() {
    if (!validationTextareField(inputValue, true)) return;
    const decrypted = decryptText(inputValue);
    setOutputValue(decrypted);
    setErrorMessage('');
  }

  const handleInputChange = (e) => {
    setInputValue(e.target.value);
    setErrorMessage('');
  };

  return (
    <div className={styles.app_container}>
      <main className={styles.app__container__main}>
        <Header />
        <section className={styles.container_text}>
          <Textarea
            id="inputApp"
            value={inputValue}
            onChange={handleInputChange}
            placeholder="Digite seu texto"
            className={styles.container__text__input}
          />
          {errorMessage && <p className="error-message">{errorMessage}</p>}
          <div className={styles.container__info__type__text}>
            <div className={styles.content__info__type__text}>
              <img src={bi_exclamation} alt="Aviso do tipo de texto a ser inserido no input" />
              <p className={styles.paragraph__info__type__text}>
                Apenas letras minúsculas e sem acento.
              </p>
            </div>
          </div>
          <div className={styles.btn}>
            <Button
              id="codificador"
              label="Criptografar"
              onClick={handleEncrypt}
              className={styles.btn__encrypt}
            />
            <Button
              id="descriptografar"
              label="Descriptografar"
              onClick={handleDecrypt}
              className={styles.btn__decrypt}
            />
          </div>
        </section>
        <Decoder outputValue={outputValue} />
      </main>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Decoder: O Princípio da Substituição de Liskov (LSP), garante componentes flexíveis e fáceis de reutilizar. No desenvolvimento com React, ele nos incentiva a usar a composição em vez da herança, permitindo que diferentes partes do código possam ser facilmente substituídas sem causar problemas à aplicação.

export function Decoder({ outputValue }) {
  const { copySuccess, showMessage } = useCopyMessage();

  function handleCopyText() {
    copyTextToClipboard(outputValue, showMessage);
  }

  return (
    <section className={styles.container__decoder}>
      <div className={styles.content__text__decoder}>

      {outputValue === '' && (
        <>
          <img
            className={styles.img_text_decoder}
            src={text_decoder} 
            alt="Decodificador de Texto" 
          />

          <h1
            className={styles.title__text_decoder}
          >
              Nenhuma mensagem encontrada
          </h1>
        </>
      )}   

          <Textarea
            id="inputtextDecodificador"
            value={outputValue}
            onChange={e => (e.target.value)} 
            placeholder="Digite um texto que você deseja criptografar ou descriptografar"
            className={`${outputValue ? styles.input__decoder__with__text : styles.input__text__decoder}`}
          />

        <div className={styles.container_copy_button}>
          <Button
            id="inputcopytext"
            onClick={handleCopyText}
            label="Copiar"
            aria-label="Copiar"
            className={styles.btn__copy}
          />
        </div>
      </div>

      {copySuccess && ( <p className={styles.copyMessage}>{copySuccess}</p> )}
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

No Decoder, Textarea e Button são usados como subcomponentes, permitindo alterações ou substituições sem afetar o funcionamento geral.

Textarea: O Princípio de Segregação de Interface (ISP), "os clientes não devem depender de interfaces que eles não usam". No contexto de aplicações React, isso significa que os componentes não devem depender de props que não utilizam.
Esse princípio defende a minimização de dependências entre os componentes do sistema, tornando-os menos acoplados e, portanto, mais reutilizáveis e, além disso, melhorando na manutenibilidade e possibilitando maior flexibilidade.

export function Textarea({ value, onChange, placeholder, className, ...props }) {
  return (
    <textarea
      value={value}
      onChange={onChange}
      placeholder={placeholder}
      className={className}
      {...props}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

No componente Textarea, as props são limitadas ao necessário, como value, onChange, placeholder e className. Além disso, há um spread operator (...props) para permitir extensibilidade sem forçar dependências desnecessárias. Isso está em conformidade com o ISP, pois minimiza dependências e evita que o componente dependa de props que não são usadas.

Conclusão

A componentização é um dos pilares para criar aplicações robustas, manutenáveis e compreensíveis. Ao dividir o projeto em pequenas partes independentes, cada uma com uma responsabilidade clara e bem definida, estamos aplicando os princípios SOLID na prática, garantindo que cada componente tenha uma “responsabilidade única” (SRP), seja “aberto para extensão e fechado para modificação” (OCP), e dependa de abstrações ao invés de implementações concretas (DIP).

O Princípio de Substituição de Liskov (LSP) nos ajuda a garantir que os componentes sejam intercambiáveis sem quebrar a aplicação, garantindo que qualquer componente derivado possa ser utilizado no lugar do componente base, ou seja, o componente que serve como modelo ou ponto de partida para outros derivados, garantindo que o comportamento do sistema se mantenha consistente. Já o Princípio de Segregação de Interface (ISP) assegura que os componentes não dependam de props desnecessárias, reduzindo o acoplamento. O Princípio de Inversão de Dependência (DIP) nos guia para que os componentes dependam de abstrações, não de implementações concretas, aumentando a flexibilidade e facilitando a manutenção e teste do sistema.

Componentizar é aplicar SOLID de forma que cada parte do sistema possa evoluir independentemente, mantendo a simplicidade e evitando a "patternite" — que é o uso forçado de padrões, complicando o código ao ponto de apenas o autor compreendê-lo. Componentização bem feita, junto aos princípios SOLID, torna o sistema não só mais robusto, como também previsível, legível, flexível, fácil de manter e de testar.

Bibliografia

Documentação: https://pt-br.react.dev/blog/2023/03/16/introducing-react-dev
Rocketseat: https://blog.rocketseat.com.br/react-do-zero-componentizacao-propriedades-e-estado/
Repositório GitHub: https://github.com/Safnaj/React-SOLID-Principles/tree/main/src/principles/LSP

Top comments (0)