DEV Community

João
João

Posted on

React: Criando Hooks Customizados

Uma das características mais importantes que um código pode ter é o reuso. É bom poder encapsular funcionalidades específicas em uma classe ou função. Esse é, na minha opinião, um probleminha com componentes de classe no React. Você precisa usar métodos do ciclo de vida do componente, como componentDidMount e componentWillUnmount. Fica mais difícil encapsular tudo isso. Você precisa alterar seus componentes, mudando a hierarquia da classe, adicionando contexts com providers ou algo do tipo. Não há algo que você possa simplesmente "plugar" no seu componente.

É aqui que o uso de componentes de função pode vir a calhar, já que os Hooks e Effects do React podem ser muito bem aproveitados. O objetivo deste pequeno artigo é mostrar como você pode utilizar encapsular específicas para sua aplicação lançando mão de Hooks. Por exemplo, o componente abaixo, extraído de um caso de uso real, permite ao usuário a entidade atual (um usuário, um aluno, um professor, uma empresa, etc):

export const FavoriteEntitiesSection = () => {
  const [favoriteEntities, setFavoriteEntities] = useState([]);
  const [currentEntityIsFavorite, setCurrentEntityIsFavorite] = useState(false);
  const currentEntity = useSelector((state) => state.currentEntity);

  const refreshFavoriteEntities = () => {
    FavoriteEntities.fetchCurrent().then(setFavoriteEntities);
  };

  useEffect(() => {
    refreshFavoriteEntities();
    return () => {
      FavoriteEntities.someCleanupAction();
    };
  }, []);

  useEffect(
    () => {
      const isFavorite = favoriteEntities.some((item) => item.id === currentEntity.id);
      setCurrentEntityIsFavorite(isFavorite);
    },
    [favoriteEntities, currentEntity],
  );

  const handlePinEntityButtonClick = () => {
    FavoriteEntities.addEntity({
      currentEntity,
    }).then(refreshFavoriteEntities);
  };

  const handleRemoveEntity = (id: string): void => {
    FavoriteEntities.removeFavoriteEntity(id).then(refreshFavoriteEntities);
  };

  return (
    <div>
      <div>Id: {currentEntity.id}</div>
      <div>Name: {currentEntity.name}</div>
      <div>
        <button disabled={currentEntityIsFavorite} onClick={handlePinEntityButtonClick}>
          Pin Entity
        </button>
      </div>
      <div>
        {favoriteEntities.map((entity) => (
          <div>{entity.name} <button onClick={() => removeEntity(entity.id)}>Remove</button></div>
        ))}
      </div>
    </div>
  );
};

E se nós quisermos usar esses favoritos em outro lugar? Poderíamos simplesmente incluir esse componente inteiro, mas pode ser que nós queiramos somente a listagem, pode ser que queiramos somente saber se a entidade atual está nos favoritos, etc. Isso pode ser resolvido incluindo tudo no Redux? Sim, mas nem sempre isso é desejável. Vamos refatorar esse código para que a funcionalidade de "favoritos" fique encapsulada:

function useFavoriteEntities(loadAtMount = true) {
  const [favoriteEntities, setFavoriteEntities] = useState([]);
  const [currentEntityIsFavorite, setCurrentEntityIsFavorite] = useState(false);
  const [isRefreshing, setIsRefreshing] = useState(false);
  const currentEntity = useSelector((state) => state.currentEntity);

  const refreshFavoriteEntities = () => {
    if (!isRefreshing) {
      setIsRefreshing(true);
      FavoriteEntities.fetchCurrent()
        .then(setFavoriteEntities)
        .finally(() => setIsRefreshing(false));
    }
  };

  useEffect(() => {
    if (loadAtMount) {
      refreshFavoriteEntities();
    }
    return () => {
      FavoriteEntities.someCleanupAction();
    };
  }, []);

  useEffect(
    () => {
      const isFavorite = favoriteEntities.some((item) => item.id === currentEntity.id);
      setCurrentEntityIsFavorite(isFavorite);
    },
    [favoriteEntities, currentEntity],
  );

  const saveCurrentEntity = () => {
    if (!currentEntityIsFavorite) {
      FavoriteEntities.addEntity({
        currentEntity,
      }).then(refreshFavoriteEntities);
    }
  };

  const removeEntity = (id) => {
    if (currentEntityIsFavorite) {
      FavoriteEntities.removeEntity({
        id,
      }).then(refreshFavoriteEntities);
    }
  };

  return {
    favoriteEntities,
    refreshFavoriteEntities,
    currentEntityIsFavorite,
    saveCurrentEntity,
    removeEntity,
  };
}

export const FavoriteEntitiesSection = () => {
  const {
    favoriteEntities,
    currentEntityIsFavorite,
    saveCurrentEntity,
    removeEntity,
  } = useFavoriteEntities(true);

  const currentEntity = useSelector((state) => state.currentEntity);


  const handlePinEntityButtonClick = () => {
    saveCurrentEntity();
  };

  const handleRemoveEntity = (id: string): void => {
    removeEntity(id);
  };

  return (
    <div>
      <div>Id: {currentEntity.id}</div>
      <div>Name: {currentEntity.name}</div>
      <div>
        <button disabled={currentEntityIsFavorite} onClick={handlePinEntityButtonClick}>
          Pin Entity
        </button>
      </div>
      <div>
        {favoriteEntities.map((entity) => (
          <div>{entity.name} <button onClick={() => handleRemoveEntity(entity.id)}>Remove</button></div>
        ))}
      </div>
    </div>
  );
};

Perceba como o componente deixa de se preocupar com a lógica em torno dos favoritos, inclusive com a atualização automática: tudo é feito dentro da função. Fica até mais fácil dar uma incrementada nas funcionalidades, como eu fiz no exemplo acima:

  • Chamadas repetidas ao refreshFavoriteEntities não vão gerar chamadas desnecessárias ao serviço.
  • Tentativas de salvar um favorito que já foi salvo não geram duplicações

Além disso. se em algum outro componente precisar somente da listagem, basta usar:

const { favoriteEntities } = useFavoriteEntities();

Espero que esse exemplo ajude a entender como tirar vantagem dos Hooks no React! Afinal, não pode ser só choro e ranger de dentes, de vez em quando tem que haver alguma vantagem.

Top comments (0)