Executando verificação de segurança...
18

3 coisas que você (provavelmente) não sabia sobre promises em JS

1. Promisificando o setTimeout

O setTimeout, apesar de ser bastante utilizado, ainda conta com uma API callback-based, ou seja, somos obrigados a passar a continuação que será executada depois do timeout estipulado na forma de callback, o que por vezes pode ser inconveniente, principalmente se precisarmos encadear várias chamadas de setTimeout:

// Famoso callback hell
setTimeout(() => {
  // Primeiro timeout
  // ...

  setTimeout(() => {
    // Segundo timeout
    // ...

    setTimeout(() => {
      // Terceiro timeout
      // ...
    }, 1000);
  }, 1000);
}, 1000);

No entanto, é possível criarmos uma "versão" do setTimeout que é promise-based, isto é, que retorna uma promise que será resolvida após o timeout estipulado:

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const main = async () => {
  // Como o `wait` agora retorna uma promise,
  // podemos utilizá-lo com async/await

  await wait(1000);
  // Primeiro timeout
  // ...

  await wait(1000);
  // Segundo timeout
  // ...

  await wait(1000);
  // Terceiro timeout
  // ...
};

Neste implementação específica, o único caveat é que não é possível cancelar o timeout, dado que não temos acesso ao timer id retornado pelo setTimeout.

Caso seja necessário cancelar o timeout, podemos uttilizar a seguinte implementação:

const wait = (ms) => {
  let timeoutId;

  const promise = new Promise((resolve) => {
    timeoutId = setTimeout(resolve, ms);
  });

  const cancel = () => clearTimeout(timeoutId);

  return {
    promise,
    cancel,
  };
};

const main = async () => {
  const { promise, cancel } = wait(1000);

  // ...
  cancel();
};

2. Resolvendo/rejeitando promises de forma programática

Por vezes, nós precisamos resolver ou rejeitar promises de forma programática, como por exemplo quando queremos testar o que acontece com algum componente ou parte da nossa aplicação quando uma promise está pendente.

A ideia consiste em extrair o resolver/rejecter da promise em questão para que então possamos chamá-los de forma programática em qualquer parte da aplicaçao:

let resolver;
let rejecter;

const promise = new Promise((resolve, reject) => {
  resolver = resolve;
  rejecter = reject;
});

// ... Em outro local da aplicação

resolver("Promise resolvida");

// Ou então

rejecter("Promise rejeitada");

Por exemplo, digamos que temos o seguinte componente React, que busca a cotação do dólar e exibe na tela:

export const ExchangeRate = () => {
  const [exchangeRate, setExchangeRate] = useState(undefined);
  const [isLoading, setIsLoading] = useState(false);

  const getExchangeRate = async () => {
    setIsLoading(true);

    try {
      const exchangeRate = await fetchExchangeRate();
      setExchangeRate(exchangeRate);
    } catch {
      alert("Erro ao buscar cotação");
    }

    setIsLoading(false);
  };

  return (
    <div>
      <button data-testid="exchange-rate-button" onClick={getExchangeRate}>
        Buscar Cotação
      </button>

      {isLoading && <p data-testid="loading">Carregando...</p>}

      {!isLoading && exchangeRate !== undefined && (
        <p data-testid="exchange-rate">A cotação de hoje é: R${exchangeRate}</p>
      )}
    </div>
  );
};

Para testar o comportamento do componente quando a promise está pendente, e, posteriormente, o que acontece quando ela resolve ou é rejeitada, podemos extrair o resolver da promise retornada pelo fetchExchangeRate e chamá-los de forma programática:

let resolver;

jest.mock("./fetchExchangeRate", () => ({
  fetchExchangeRate: () =>
    new Promise((resolve) => {
      // Extraindo resolver
      resolver = resolve;
      rejecter = reject;
    }),
}));

describe("When user clicks on 'Buscar Cotação'", () => {
  it("Displays a loading message and then displays the exchange rate", async () => {
    render(<ExchangeRate />);

    fireEvent.click(screen.getByTestId("exchange-rate-button"));

    // Checa se o componente de loading está na tela
    expect(screen.getByTestId("loading")).toBeInTheDocument();

    // Resolve a promise
    resolver(5.5);

    expect(await screen.findByTestId("exchange-rate")).toHaveTextContent(
      "A cotação de hoje é: R$5.5",
    );
  });
});

3. Encadeando continuações em promises já resolvidas/rejeitadas

Eu não sei você, mas quando eu comecei a mexer com promises em JS, uma das coisas com as quais eu me preocupava era a seguinte: "E se eu chamar then em uma promise que já foi resolvida/rejeitada? O que acontece? Será que a continuação vai ser simplesmente engolida?".

Felizmente a resposta é não!

Uma das garantias que a promise nos dá é que as continuações que nós encadeamos nela serão sempre executadas, independentemente da promise já ter sido resolvida/rejeitada ou não.

Exemplo:

// Promise já resolvida
const promise = Promise.resolve(42);

promise.then(() => console.log(1));
promise.then(() => console.log(2));
promise.then(() => console.log(3));

// Logs:
// 1
// 2
// 3

// Promise já rejeitada
const otherPromise = Promise.reject(new Error("Oops"));

otherPromise.catch(() => console.log(1));
otherPromise.catch(() => console.log(2));
otherPromise.catch(() => console.log(3));

// Logs:
// 1
// 2
// 3

Demo: https://stackblitz.com/edit/typescript-mscxmt?file=index.ts

Outro

Se você tem interesse em fazer um mergulho mais profundo em promises e async de forma geral, dê uma olhada nesse repositório: https://github.com/henriqueinonhe/promises-training.

Ele contém uma série de exercícios práticos envolvendo promises, direcionados para quem já tem um conhecimento básico sobre o assunto mas quer se aprofundar.

Vale notar que cada exercício conta com uma bateria de testes que você pode rodar para verificar se a sua solução está correta ou não.

0