Refatoração: Separando lógica de componentes React

7/28/2021

React Hooks são abstrações úteis para o gerenciamento de estado e efeitos colaterais. Isso significa que você usará hooks somente para lidar com estado ou para gerenciar efeitos colaterais como processos assíncronos dependentes.

Você pode acessar o sandbox deste exemplo no CodeSandbox

Considere o Componente a seguir:

function App() {
  const [state, setState] = React.useState({
    email: "",
    password: ""
  });

  const isValid =
    !!state.email && state.password.length > 12;

  const setStateProp = (prop) => (ev) =>
    setState({
      ...state,
      [prop]: ev.target.value
    });

  return (
    <form className="form">
      <input
        placeholder="E-mail"
        type="email"
        value={state.email}
        onChange={setStateProp("email")}
      />

      <input
        placeholder="Password"
        type="password"
        value={state.password}
        onChange={setStateProp("password")}
      />

      <button disabled={!isValid}>Login</button>
    </form>
  );
}

Este componente representa um formulário de login que contém um campo de e-mail, um campo de senha e um botão de login. Todo gerenciamento de estado é feito dentro do componente usando o hook useState.

A representação de estado é feita por um objeto que contém as propriedades email e password. Cada propriedade possui uma string vazia como valor inicial.

O problema desde componente é que ele contém lógica de gerenciamento de estado e código responsável por montar a parte visível ao usuário final. Misturar lógica de gerenciamento de estado e visualização leva rapidamente a problemas de manutenção do código fonte.

A saída aqui é mover as linhas de código que lidam com a lógica de estado para seu próprio contexto. O React nos permite fazer isso com custom hooks.

O primeiro passo é ter uma suíte de testes funcionando, para que você tenha certeza de que não houve regressões durante a refatoração.

O segundo passo é criar o novo hook. Vou chamar de useFormState:

function useFormState() {}

Vejo os meus testes e tudo está funcionando.

Esse Hook precisa retornar funções para que seja possível operar no formulário. Serei metodológico e irei mover as linhas de código que lidam com o gerenciamento de estado do formulário para esta nova função:

function useFormState() {
  const [state, setState] = React.useState({
    email: "",
    password: ""
  });
  
  const isValid =
    !!state.email && state.password.length > 12;
  
  const setStateProp = (prop) => (ev) =>
    setState({
      ...state,
      [prop]: ev.target.value
    });
}

Neste momento meus testes falham, justamente porque agora o meu componente não possui mais as variáveis e funções que lidavam com o estado:

export default function App() {
  return (
    <form className="form">
      <input
        placeholder="E-mail"
        type="email"
        value={state.email}
        onChange={setStateProp("email")}
      />
  
      <input
        placeholder="Password"
        type="password"
        value={state.password}
        onChange={setStateProp("password")}
      />
  
      <button disabled={!isValid} title="login">
        Login
      </button>
    </form>
  );
}

Neste momento, restam dois passos. O primeiro passo é retornar o estado, a função setStateProp e a variável isValid na função useFormState. O segundo passo é usar esses valores no hook. Farei as duas alterações ao mesmo tempo para ter os meus testes funcionando novamente. Este é o resultado final:

function useFormState() {
  const [state, setState] = React.useState({
    email: "",
    password: ""
  });
  
  const isValid =
    !!state.email && state.password.length > 12;
  
  const setStateProp = (prop) => (ev) =>
    setState({
      ...state,
      [prop]: ev.target.value
    });
  
  return { state, isValid, setStateProp };
}
  
export default function App() {
  const { state, isValid, setStateProp } = useFormState();
  
  return (
    <form className="form">
      <input
        placeholder="E-mail"
        type="email"
        value={state.email}
        onChange={setStateProp("email")}
      />
  
      <input
        placeholder="Password"
        type="password"
        value={state.password}
        onChange={setStateProp("password")}
      />
  
      <button disabled={!isValid} title="login">
        Login
      </button>
    </form>
  );
}

O que eu ganhei com essa mudança?

O primeiro ganho foi a flexibilidade em lidar com o estado do formulário. Desde que eu mantenha a mesma API, qualquer alteração no Hook useFormState não vai necessitar qualquer mudança na visualização do formulário.

O segundo ganho é que lógica de gerenciamento de estado e visualização estão desacoplados. Posso adicionar mais elementos visuais no formulário, como mensagens para instrução e isso em nada vai impactar o código que representa a lógica de gerenciamento de estado.

Porem escolho não parar por aqui.

Por preferência pessoal, dentro do Hook useFormState, vou definir uma função onde vai ser possível acessar um parâmetro do estado do formulário e uma função para alterar o valor deste parâmetro.

function useFormState() {
  const [state, setState] = React.useState({
    email: "",
    password: ""
  });

  const isValid =
    !!state.email && state.password.length > 12;

  const setStateProp = (prop) => (ev) =>
    setState({
      ...state,
      [prop]: ev.target.value
    });
  
  const valueOf = (prop) => {}

  const setValueOf = (prop, value) => {}

  return { state, isValid, setStateProp };
}

Essas funções serão responsáveis por acessar e manipular o estado do formulário. A função valueOf vai receber uma propriedade do estado do formulário e vai retornar seu valor caso existir.

const valueOf = (prop) => state[prop]

A função setValueOf vai receber o nome da propriedade e o seu novo valor e então vai causar uma mutação no valor corrente do estado.

const setValueOf = (prop, value) =>
    setState({ ...state, [prop]: value });

Vou modificar o retorno do Hook para que seja possível acessar estas duas novas funções no meu componente de formulário de login:

function useFormState() {
  const [state, setState] = React.useState({
    email: "",
    password: ""
  });

  const isValid =
    !!state.email && state.password.length > 12;

  const setStateProp = (prop) => (ev) =>
    setState({
      ...state,
      [prop]: ev.target.value
    });

  const valueOf = (prop) => state[prop];

  const setValueOf = (prop, value) =>
    setState({ ...state, [prop]: value });

  return {
    state,
    isValid,
    setStateProp,
    valueOf,
    setValueOf
  };
}

Agora posso alterar o meu componente e usar as novas funções:

export default function App() {
  const { valueOf, setValueOf, isValid } = useFormState();

  return (
    <form className="form">
      <input
        placeholder="E-mail"
        type="email"
        value={valueOf("email")}
        onChange={(ev) =>
          setValueOf("email", ev.target.value)
        }
      />

      <input
        placeholder="Password"
        type="password"
        value={valueOf("password")}
        onChange={(ev) =>
          setValueOf("password", ev.target.value)
        }
      />

      <button disabled={!isValid} title="login">
        Login
      </button>
    </form>
  );
}

Como não é do meu interesse manter a compatibilidade com a API anterior, removo as funções anteriores do Hook useFormState:

function useFormState() {
  const [state, setState] = React.useState({
    email: "",
    password: ""
  });

  const isValid =
    !!state.email && state.password.length > 12;

  const valueOf = (prop) => state[prop];

  const setValueOf = (prop, value) =>
    setState({ ...state, [prop]: value });

  return {
    isValid,
    valueOf,
    setValueOf
  };
}

Verifico novamente os meus testes e está tudo dentro do esperado. Porém há algo no código atual que está me incomodando muito. Primeiro é o Hook useFormState usar um estado inicial constante. Não é possível alterar o estado inicial, mesmo que as funções que manipulam este estado sejam flexíveis.

Outro ponto é que a lógica de validação está acoplada ao estado inicial. Tanto o estado inicial quanto a lógica de validação podem ser removidos do código do Hook.

Começo aceitando um objeto como valor inicial para evitar que os meus testes não falhem, pelo menos não agora:

function useFormState(
  initialState = {
    email: "",
    password: ""
  }
) {
  const [state, setState] = React.useState(initialState);
  
  const isValid =
    !!state.email && state.password.length > 12;
  
  const valueOf = (prop) => state[prop];
  
  const setValueOf = (prop, value) =>
    setState({ ...state, [prop]: value });
  
  return {
    isValid,
    valueOf,
    setValueOf
  };
}

Agora movo a inicialização do estado inicial para o Componente. É o componente que precisa saber qual será o estado inicial.

O trabalho do Hook é gerenciar o estado de acordo com as ações do usuário.

export default function App() {
  const { valueOf, setValueOf, isValid } = useFormState({
    email: "",
    password: ""
  });
  
  return (
    <form />
  );
}

O segundo passo é aceitar um validador como segundo argumento do meu Hook. Vou chamar este parâmetro de validator:

function useFormState(
  initialState,
  validator = () => true
) {
  const [state, setState] = React.useState(initialState);
  
  const isValid =
    !!state.email && state.password.length > 12;
  
  const valueOf = (prop) => state[prop];
  
  const setValueOf = (prop, value) =>
    setState({ ...state, [prop]: value });
  
  return {
    isValid,
    valueOf,
    setValueOf
  };
}

Por padrão, o validador é uma função que retorna um valor booleano. Quando o cliente desde código não passar nenhum validador, isso significa que nenhuma validação será feita. Esta é a minha opinião sobre o código, todo desenvolvedor deveria ser opinativo quanto ao design e as decisões sobre o código.

A função de validação vai receber o estado como argumento e vai retornar um booleano. Modifico o meu Hook para usar a função de validação e movo o código que executa a validação para o cliente do meu componente, que atualmente é o componente do formulário de login.

function useFormState(
  initialState,
  validator = () => true
) {
  const [state, setState] = React.useState(initialState);
  const isValid = validator(state);
  
  const valueOf = (prop) => state[prop];
  
  const setValueOf = (prop, value) =>
    setState({ ...state, [prop]: value });
  
  return {
    isValid,
    valueOf,
    setValueOf
  };
}
  
export default function App() {
  const validate = (state) =>
    !!state.email && state.password.length > 12;
  const { valueOf, setValueOf, isValid } = useFormState(
    {
      email: "",
      password: ""
    },
    validate
  );
  
  return (
    <form />
  );
}

A função de validação do componente é útil apenas para o componente. O gerenciador de estado não deveria saber o que é um estado válido ou inválido. A validade da informação depende da perspectiva de quem está consultando aquela informação.

Como o contexto da função validate depende apenas de seu argumento, posso remover essa função do componente:

const validate = (state) =>
  !!state.email && state.password.length > 12;
  
export default function App() {
  const { valueOf, setValueOf, isValid } = useFormState(
    {
      email: "",
      password: ""
    },
    validate
  );
  
  return (
    <form />
  );
}

O valor inicial do estado também não possui dependências, então posso movê-lo para fora do componente.

const initialState = {
  email: "",
  password: ""
};
  
const validate = (state) =>
  !!state.email && state.password.length > 12;
  
export default function App() {
  const { valueOf, setValueOf, isValid } = useFormState(
    initialState,
    validate
  );
  
  return (
    <form />
  );
}

Agora posso isolar o hook useFormState em seu arquivo e usá-lo em qualquer outro componente que precise da mesma lógica de gerenciamento de formulário.

👈 Todos os artigos📝 Edite esta página

🤞

Tenha um dia incrível