Utilizando o useEffect ao seu favor com useEffectByStatus
Tags: react, typescript, frontend, hooks
Published: 2021-08-18 14:59:08

Sou desenvolvedor em uma empresa que utiliza React e Relay no frontend, consumindo API's em GraphQL. Na aplicação, existe um hook customizado chamado `useMutation`, que me retorna uma função para executar a mutation, além de o status atualizado da mesma e os dados retornados por ela. Esse status é utilizado pra colocar a página em estado de loading, por exemplo.

Pois bem, vamos supor que eu preciso utilizar esse status pra realizar ações na minha página. Caso estivesse utilizando diretamente o retorno da mutation, poderia fazer algo assim:

```typescript
const mutation = graphql`
  mutation addUserMutation($input: AddUserInput!) {
    user {
      addUser(input: $input) {
        user {
          id
          name
        }
        responseEnum
      }
    }
  }
`;

const Page = () => {
  const [notification, setNotification] = useState(null);
  const [pageStatus, setPageStatus] = useState('idle');
  const [name, setName] = useState('');
  const { submit } = useMutation(mutation);
  
  const submitMutation = async () => {
    setNotification(null);
    setPageStatus('loading');
  
    const response = await submit({ name });
  
    if (response === 'resolved') {
      setName('');
      // update parent component table
    }
  
    if (response === 'error') {
      setNotification({ error: response.user.addUser.responseEnum });
    }
  
    setPageStatus('idle');
  }
}
```

Isso funciona bem e é um bom exemplo de uso da programação imperativa. Mas como gosto de usar ao máximo os retornos desse hook criado pelos meus colegas, normalmente utilizo desta forma:

```typescript
const Page = () => {
  const [notification, setNotification] = useState(null);
  const [pageStatus, setPageStatus] = useState('idle');
  const [name, setName] = useState('');
  const { submit, status, data } = useMutation(mutation);

  useEffect(() => {
    if (status === 'idle') {
      setNotification(null);
      setPageStatus('idle');
    }

    if (status === 'loading') {
      setNotification(null);
      setPageStatus('loading');
    }

    if (status === 'resolved') {
      setPageStatus('idle');
      setName('');
      // update parent component table
    }

    if (status === 'error') {
      setNotification({ error: data.user.addUser.responseEnum });
      setPageStatus('idle');
    }
  }, [status]);
  
  const submitMutation = () => submit({ name });
}
```

A função `submitMutation` agora apenas chama a função `submit` do `useMutation`, deixando que o `useEffect` que está enxergando as alterações de estado do status faça a maior parte do trabalho.

Porém, além de querer tornar esse `useEffect` menos repetitivo, eu queria criar algo que pudesse ser utilizado em outras páginas, pois o hook `useMutation` é chamado em diversos lugares da aplicação. Além disso, sempre busco deixar meu código mais elegante, pois acredito que um código estéticamente interessante torna a leitura mais prazerosa e a manutenção mais tranquila. Por fim, tenho buscado uma abordagem menos imperativa, utilizando mais o conceito de programação declarativa que permeia o React.

Pensando em tudo isso, nasce o `useEffectByStatus`:

```typescript
type StatusType = 'idle' | 'loading' | 'resolved' | 'error'; 

type UseEffectByStatusProps = {
  status: StatusType;
  effects: {
    [key in StatusType]?: () => void;
  };
};

const useEffectByStatus = (props: UseEffectByStatusProps) => {
  const { status, effects } = props;

  useEffect(() => {
    effects[status]?.();
  }, [status]);
};
```

Basicamente, a ideia é que ele contenha um `useEffect` que dispare uma função de acordo com o status atual passado para dentro do hook. O `optional chaining` é usado pra validar se foi passada uma função effect como parâmetro para o status em que o hook se encontra, pois não é obrigatória a passagem de uma função para todos os status existentes. Além disso, com a aplicação do padrão `Object Literal`, não há necessidade de condicionais (`if` ou `switch case`).

Com a utilização deste novo hook, a página ficou assim:
```typescript
const Page = () => {
  const [notification, setNotification] = useState(null);
  const [pageStatus, setPageStatus] = useState('idle');
  const [name, setName] = useState('');
  const { submit, status, data } = useMutation(mutation);

  useEffectByStatus({
    status,
    effects: {
      idle: () => {
        setNotification(null);
        setPageStatus('idle');
      },
      loading: () => {
        setNotification(null);
        setPageStatus('loading');
      },
      resolved: () => {
        setPageStatus('idle');
        setName('');
        // update parent component table
      },
      error: () => {
        setNotification({ error: data.user.addUser.responseEnum });
        setPageStatus('idle');
      },
    }
  });
  
  const submitMutation = () => submit({ name });
}
```

Com ajuda do meu Tech Lead e parceiro @samuelmpinho, o hook se tornou ainda mais genérico, pois passou a sugerir através do Typescript as possíveis opções de effects de acordo com o status passado no primeiro parâmetro, o que só é possível pelo uso do `Object Literal`:

```typescript
type UseEffectByStatusProps<T extends string> = {
  status: T;
  effects: {
    [key in T]?: () => void;
  };
};

const useEffectByStatus = <T extends string>(props: UseEffectByStatusProps<T>) => {
  const { status, effects } = props;

  useEffect(() => {
    effects[status]?.();
  }, [status]);
};
```

O resultado é um `autocomplete` adaptável ao status passado para o hook:
![Autocomplete com as funções effect de acordo com o status](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/whp4b96d9lolcl5sihkr.png)

Se tiverem sugestões de como melhorar ou outras aplicações deste hook, vou ficar feliz em saber! Me manda uma mensagem no [Twitter](https://twitter.com/rodolfoviolla) pra gente conversar. Até a próxima! :rocket: