Utilizando o useEffect ao seu favor com useEffectByStatus

rodolfoviolla

Rodolfo Della Violla

Posted on August 18, 2021

Utilizando o useEffect ao seu favor com useEffectByStatus

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:

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

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:

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

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:

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

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:

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

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:

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

O resultado é um autocomplete adaptável ao status passado para o hook:
Autocomplete com as funções effect de acordo com o status

Se tiverem sugestões de como melhorar ou outras aplicações deste hook, vou ficar feliz em saber! Me manda uma mensagem no Twitter pra gente conversar. Até a próxima! 🚀

💖 💪 🙅 🚩
rodolfoviolla
Rodolfo Della Violla

Posted on August 18, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related