Utilizando o <Suspense> no ReactJS

Utilizando o <Suspense> no ReactJS

Olá leitor, seja bem vindo a mais uma aula, onde nós iremos aprender sobre um componente bastante peculiar no universo do ReactJS, chamado de <Suspense>.

Mas sem fazer suspense, ok?

Em conteúdos anteriores, você aprendeu a criar requisições para web utilizando algumas bibliotecas como:

Todas as vezes em que precisamos realizar uma requisição para um serviço externo (tarefa que pode ser demorada), nós aprendemos a chamar uma função assíncrona que em conjunto com os estados da sua aplicação, irão fazer com que a UI (interface do usuário), não fique travada até que a requisição seja completada, e com isso, os dados sejam finalmente mostrados para o seu usuário.

Ou seja, criamos uma função que por de baixo dos panos vai ficar responsável por colher alguns dados de uma API, e quando tudo estiver pronto, ela só atualiza o conteúdo na UI.

Mas você também pode travar (congelar 🥶) a sua aplicação se preferir rs

Eu te garanto que é muito mais fácil chamar um componente <Loading> , ou quem sabe mostrar uma mensagem de "carregando" para o seu usuário, não é verdade?

Essa brincadeira ficaria mais ou menos assim (em conjunto com o useEffect):

const listaDeNomes = () => {
 const [loading, setLoading] = useState(true);
 const [lista, setLista] = useState({});

 useEffect(()=>{
 fetch("http://micilini.com/pega-nomes").then((result)=>{
 setLoading(false)
 setLista(result.lista);
 })
 },[]);

 if(loading){
 return <div>A lista de nomes ainda está sendo carregada...</div>
 }

 return (
 <>
 <div>{/* Sua lógica para mostrar a lista de nomes! */}</div>
 </>
 )
}

Pensando em melhorar cada vez mais a forma com que escrevemos nossos códigos, a equipe de desenvolvimento do ReactJS criou uma nova funcionalidade chamada de <Suspense>.

Na qual iremos descobrí-la agora 😜

O que é o <Suspense>?

O React Suspense nada mais é do que um novo recurso do ReactJS que está presente deste a sua versão 16.6.

Graças a ele, agora nossa aplicações poderão pausar a renderização de um determinado componente, enquanto aguardam a conclusão de um processo assíncrono (chamada de uma API, por exemplo).

Ele foi desenvolvido com o objetivo de facilitar a criação de aplicações que dependiam do carregamento de dados externos.

Com o <Suspense>, você pode interromper a renderização da árvore dos seus componentes, até que alguns critérios existentes na sua aplicação sejam atendidos.

Mas espera aí... o useEffect em conjunto com estratégias de loading, já não funcionam muito bem?

Sim, pois como você viu anteriormente, podemos criar esses mecanismos de carregamento por meio de renderização condicional em conjunto com o gerenciamento de estados.

Beleza... mas qual o problema disso?

O problema é: Quanto mais complexa for a lógica de renderização condicional e o gerenciamento de estados, maior a probabilidade de introduzirmos bugs difíceis de rastrear.

Isso acontece porque a complexidade aumenta a interdependência entre os diferentes componentes e estados da nossa aplicação.

Além disso, quanto mais complexa a lógica, mais difícil pode ser para novos desenvolvedores entenderem o código existente e fazerem modificações sem introduzir novos problemas.

Imagina comigo... um único componente contendo diversos loading de estado diferentes...

O quão poluído seria esse código? Consegue imaginar? Nem eu... 😵‍💫

O pior disso tudo, não é nem a bagunça em spi, mas sim a grande probabilidade de afetar o desempenho da nossa aplicação, já que renderizações condicionais mais complexas podem exigir mais processamento.

E é por esse motivo que a equipe de desenvolvimento do ReactJS resolveu lançar o <Suspense>, pois com ele temos um método mais sofisticado e integrado de gerenciar ações assíncronas dentro da nossa aplicação.

Isso significa que tudo o que eu aprendi até agora na jornada do ReactJS no portal da Micilini, está errado?

É claro que não! Visto que você ainda pode continuar gerenciando suas funções assíncronas da maneira como vem fazendo (usando gerencimanto de estados).

O objetivo deste tópico é mostrar para você um método mais sofisticado de se lidar com suas operações assíncronas, e você pode decidir por si mesmo se prefere fazer o uso delas ou não 😌

O componente <Suspense>

Com o componente <Suspense>, você pode declarar um conteúdo substituto, que será mostrado para o seu usuário, enquanto o conteúdo principal ainda está realizando operações assíncronas, de modo que quando ele terminar tais operações, o conteúdo seja finalmente mostrado.

Para usar esse componente dentro do seu projeto em ReactJS, basta importá-lo da seguinte forma:

import { Suspense } from 'react';

A estrutura do componente <Suspense> se dá na seguinte estrutura:

<Suspense fallback={<ListaCarregando />}>
 <ListaComponente />
</Suspense>

No caso do exemplo acima, estamos declarando um componente chamado <Suspense> que inicialmente vai carregar e mostrar o conteúdo do componente <ListaCarregando /> enquanto o componente <ListaComponente> ainda estiver realizando suas operações assíncronas.

Ou seja, enquanto o componente <ListaComponente> não estiver pronto, o <Suspense> vai mostrar o conteúdo existente dentro de <ListaCarregando>.

Vamos ver agora uma representação um pouco mais simples:

<Suspense fallback={<div>A lista está carregando...</div>}>
 <ListaDeNomes />
</Suspense>

Ficou mais entendível agora, não é verdade?

Beleza... mas de que forma o <Suspense> sabe que o componente <ListaDeNomes> está carregando algo de forma assíncrona? Eu preciso fazer com que o <ListaDeNomes> retorne algum tipo de callback?

Não, você não precisa que o componente <ListaDeNomes> retorne algum dado de volta ao componente pai (<Suspense>), pois ele ja é inteligente o suficiente para entender que existem Promises que estão sendo executadas dentro do componente.

Maaaaas, não é só criar o componente de forma fácil como estavámos fazendo anteriormente, uma vez que, para que tudo funcione perfeitamente, você precisa fazer o uso de dois hacks:

  • Utilizar um wrapPromise
  • Ou, fazer o uso do Lazy()

Vamos entender mais a fundo o funcionamento de cada um deles, mas antes, vamos configurar o nosso projeto inicial 😎

Criando seu projeto de testes

Antes de começarmos, vamos criar um novo projeto em ReactJS dedicado a esta lição. No meu caso criei um novo projeto chamado de componente-suspense:  

npx create-react-app componente-suspense

Após isso, não se esqueça de fazer aquela limpeza do código, e também aquela organizada nas pastas do src (organização por funcionalidade) 😁

E também o Axios, pois iremos utilizá-lo também:

npm install axios

Feito isso, partiu aprender a criar um wrapPromise!

Criando um wrapPromise dentro do seu componente

Um wrapPromise nada mais é do que um arquivo Javascript que deve estar localizado dentro da pasta de utilidades (famosa pasta utils).

Seu objetivo é receber uma Promessa (Promisse), e executar um then() de modo a verificar o status daquela promessa, ou seja, se ocorreu tudo bem (success) ou não (error), para que em seguida o resultado dessa promessa seja retornado de volta a quem chamou o wrapPromise.

Basicamente, é como se criássemos uma constante que executa uma função anônima que contem uma promessa, onde o resultado dessa promessa é retornado de volta a constante:

Mas, você e eu sabemos que a ilustração acima está ERRADA e isso não acontece de fato! Pois o retorno será um objeto e não uma string (com o valor Micilini).

Sendo assim, se você quer enviar o valor de uma resolve (ou um reject) de volta a variável que à chama (meuNome), você vai precisar de um wrapPromise!

Vamos começar criando uma nova pasta chamada de utils dentro da pasta src do nosso projeto:

Ali dentro vamos criar um novo arquivo chamado wrapPromise.js com o seguinte conteúdo:

const wrapPromise = (promise) => {
 let status = 'pending';
 let result;
 const suspender = promise.then(
 (r) => {
 status = 'success';
 result = r;
 },
 (e) => {
 status = 'error';
 result = e;
 }
 );
 const handler = {
 pending: () => {
 throw suspender;
 },
 error: () => {
 throw result;
 },
 success: () => result,
 };
 const read = () => {
 return handler[status] ? handler[status]() : handler.success();
 }
 return { read };
};

export default wrapPromise;

Basicamente, ele é uma função que recebe uma promessa como parâmetro e executa o then(), de modo a salvar o resultado em uma variável chamada result, para posteriormente ser acessada dentro de um método read().

Para testarmos seu funcionamento, vamos criar um novo componente chamado de Nome.

Nome > index.jsx:

import wrapPromise from '../../utils/wrapPromise';

const getNome = () => {
 return new Promise((resolve, reject) => {
 setTimeout(() => {
 resolve("Micilini");
 }, 2000);
 });
}

const meuNome = wrapPromise(getNome());

const Nome = () => {
 const nome = meuNome.read();

 return(
 <div>
 <h1>{nome}</h1>
 </div>
 );
}

export default Nome;

No comando acima estamos usando o wrapPromise para chamar a função getNome(), que por sua vez, está declarada dentro do componente. Lembrando que essa função poderia estar dentro de uma classe em outro arquivo (tipo um service), ok? (veremos isso mais a frente).

E como wrapPromise retorna o resultado da promessa depois de 2 segundos , a constante meuNome vai armazenar a string (micilini).

Show, agora vamos colocar esse componente para funcionar... para isso eu precisei criar uma nova página chamada de Home (que vai conter o componente <Suspense>):

Home > index.jsx:

import { Suspense } from 'react';
import Nome from '../../components/Nome';

const Home = () => {
 return (
 <div>
 <h1>Tela Principal!</h1>
 <Suspense fallback={<div>Carregando nomes...</div>}>
 <Nome />
 </Suspense>
 </div>
 );
};

export default Home;

Por fim, não se esqueça de chamar essa página dentro do App.js:

import Home from './pages/Home';

export default function App() {
 return (
 <div>
 <Home />
 </div>
 );
}

Veja como ficou o resultado final:

Daí voce pode estar se perguntando: Por que a chamada do wrapPromise dentro da constante meuNome está declarada fora do escopo do componente Nome?

A resposta simples é: Pois se você mover essa linha para dentro do componente (Nome), ele não funcionar 😝 

Mas a resposta correta é: Quando o declaramos dentro do componente do ReactJS, ele o trata de uma outra forma, e apesar da promessa ser resolvida, a UI do usuário não é atualizada em conjunto, e acredito que você já sabe o motivo.

Quando o meuNome é declarado fora do componente, ele é calculado apenas uma vez durante a inicialização do módulo.

Porque se fossémos colocá-lo dentro do componente em sí:

const Nome = () => {
 const meuNome = wrapPromise(getNome());
 const nome = meuNome.read();
 ....
}

Primeiro que a nossa UI não seia atualizada, e a mensagem de "carregando nomes..." nunca seria chamada.

E segundo, que se mesmo assim você quisesse declarar a chamada do wrapPromise dentro do componente, você teria que fazer algo como:

import React, { useState, useEffect } from 'react';
import wrapPromise from '../../utils/wrapPromise';

const getNome = () => {
 return new Promise((resolve, reject) => {
 setTimeout(() => {
 resolve("Micilini");
 }, 2000);
 });
}

const Nome = () => {
 // Usando useState para manter o estado de meuNome
 const [meuNomePromise, setMeuNomePromise] = useState(null);

 useEffect(() => {
 // Inicializa a promise apenas uma vez
 if (!meuNomePromise) {
 setMeuNomePromise(wrapPromise(getNome()));
 }
 }, [meuNomePromise]); // Dependência vazia, para garantir que só execute uma vez

 // Acessa o valor atual da promise
 const nome = meuNomePromise && meuNomePromise.read();

 return (
 <div>
 <h1>{nome}</h1>
 </div>
 );
}

export default Nome;

Só que aí.... você vai estar fazendo o uso do useState e useEffect para controlar os estados da sua aplicação, o que implica em executar a promessa toda vez que esse componente sofre alterações de estado.

Mas aí, isso é um tipo de coisa que só você pode decidir se vale a pena ou não fazer na sua aplicação 🧐

Chamando uma API externa dentro do WrapPromisse

No exemplo anterior nós aprendemos de forma bastante simplista como utilizar o wrapPromise para retornar um nome.

Mas no mundo real, o que a gente faz mesmo são chamadas á APIs externas, mas como isso funciona?

Simples, para isso vamos criar um novo componente chamado de Nomes.

Nomes > index.jsx:

import wrapPromise from '../../utils/wrapPromise';
import getNomes from '../../services/getNomes';

const nomePromise = wrapPromise(getNomes());

const Nomes = () => {
 const nomes = nomePromise.read();

 return(
 <ul>
 {nomes.map((nome) => (
 <li key={nome.id}>{nome.name}</li>
 ))}
 </ul>
 );
}

export default Nomes;

E também vamos criar uma nova service (dentro da pasta services) chamada de getNomes.js:

const getNomes = () => {
 return new Promise(async (resolve, reject) => {
 try{
 const response = await fetch('https://jsonplaceholder.typicode.com/users');
 
 if (!response.ok) {
 throw new Error('Erro ao carregar os nomes');
 }
 
 const data = response.json();
 resolve(data);
 }catch(e){
 reject(e);
 }
 
 });
}

export default getNomes;

Por fim, não se esqueça de chamar o componente Nomes dentro da página Home:

import { Suspense } from 'react';
import Nomes from '../../components/Nomes';

const Home = () => {
 return (
 <div>
 <h1>Tela Principal!</h1>
 <Suspense fallback={<div>Carregando nomes...</div>}>
 <Nomes />
 </Suspense>
 </div>
 );
};

export default Home;

Veja como ficou o resultado final:

Sim, a atualização acontece de forma tão rápida que quase não dá pra ler direito a mensagem de carregamento.

Note que dentro da nossa promessa, nós fizemos uma chamada a uma API externa por meio do getNomes().

Mas, temos um pequeno porém: o que aconteceria se o fetch retornasse um erro? Ou seja, como o <Suspense> iria se comportar caso ele retornasse um reject ou até mesmo um throw?

Lidando com erros no wrapPromise (ErrorBoundary)

Componentes conhecidos como ErrorBoundary possuem capacidades de detectar, e gerenciar falhas que ocorrem dentro da sua aplicação, principalmente em componentes filhos (children).

Você tanto pode criar sua própria classe de erros no ReactJS, como também usar uma biblioteca muito conhecida para lidar com isso, chamada de react-error-boundary.

No nosso caso, vamos seguir pelo caminho de instalação dessa biblioteca, sendo assim, abra o prompt de comando dentro da pasta principal da sua aplicação, e execute o comando:

npm install --save react-error-boundary

Após isso, vamos criar dentro da pasta utils, mais um novo arquivo chamado de errorBoundary.js, com o seguinte conteúdo:

import { ErrorBoundary } from "react-error-boundary";

const ErrorFallback = ({ error, resetErrorBoundary }) => {
 console.log(error);
 return (
 <p>Algo deu errado!</p>
 );
}

export { ErrorBoundary, ErrorFallback };

Essa classe faz o uso da biblioteca que acabamos de instalar, e o objetivo é reconhecer erros que podem acontecer na sua aplicação, de modo a retornar o JSX que está declarado dentro do ErrorFallback.

Para testar essa função, eu criei uma nova página chamada de HomeDois.

HomeDois > index.jsx:

import { Suspense } from 'react';
import Nomes from '../../components/Nomes';
import { ErrorBoundary, ErrorFallback } from '../../utils/errorBoundary';

const Home = () => {
 return (
 <div>
 <h1>Tela Principal (Dois)!</h1>
 <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => {
 // Reseta o estado da sua aplicação para que o erro não seja exibido novamente!
 }}>
 <Suspense fallback={<div>Carregando nomes...</div>}>
 <Nomes />
 </Suspense>
 </ErrorBoundary>
 </div>
 );
};

export default Home;

Não se esqueça de fazer a chamada dessa nova pagina dentro do App.js:

import HomeDois from './pages/HomeDois';

export default function App() {
 return (
 <div>
 <HomeDois />
 </div>
 );
}

Voltando a lógica existente dentro do HomeDois, nós importamos o módulo que criamos e encapsulamentos o <Suspense> dentro do <ErrorBoundary>, de modo que se algo der errado (retornar um reject), o fallback seja chamado (ErrorFallback) mostrando um JSX.

Para simular um erro na nossa requisição, que tal chamarmos de forma errada a URL do fetch do getNomes.js?

const response = await fetch('https://jsonplaceholder.typicode.com/users3');//adicionei um 3 no final da URL, o que claramente vai dar problema no retorno

Veja como ficou o resultado final:

Ele chega a mostrar a mensagem "carregando nomes..." mas logo em seguida retorna a mensagem de erro.

E é dessa forma que usamos o wrapPromise 🙂

Utilizando o Lazy()

O ReactJS possui um mecanismo de importação dinâmica chamada de lazy(), ou cuja tradução para o bom português: PREGUIÇOSO!

Cujo o objetivo é permitir que os componentes sejam carregados de forma mais lenta, ou, somente quando ele forem necessário.

É importante ressaltar que assim como o <Suspense> essa nova funcionalidade está presente desde a versão 16.6 do ReactJS, que foi pensada para ser usada em conjunto com o <Suspense>, de forma a aumentar a velocidade das nossas aplicações web.

O que a torna muito útil para minimizar a velocidade de carregamento das nossas aplicações 😎

Para testarmos, vamos começar criando um novo componente chamado de Idade.

Idade > index.jsx:

import { lazy } from 'react';

const Idade = lazy(() => {
 return new Promise(resolve => {
 setTimeout(() => {
 resolve({
 default: () => <div>Idade: 30</div> // Simula a definição do componente Idade, mas você poderia inserir um componente, exemplo: <Idade />, de modo a passar props para ele...
 });
 }, 2000);
 });
});

export default Idade;

E também uma nova página chamada de HomeTres.

HomeTres > index.jsx:

import { Suspense } from 'react';
import Idade from '../../components/Idade';

const Home = () => {
 return (
 <div>
 <h1>Tela Principal!</h1>
 <Suspense fallback={<div>Carregando idade...</div>}>
 <Idade />
 </Suspense>
 </div>
 );
};

export default Home;

Não se esqueça de importar esse componente dentro do App.js:

import HomeTres from './pages/HomeTres';

export default function App() {
 return (
 <div>
 <HomeTres />
 </div>
 );
}

Como podemos vern dentro do componente Idade estamos utilizando o lazy para retornar uma promise, que por sua vez, retorna um JSX contendo a idade do usuário.

Veja como ficou o resultado final:

Benefícios do lazy()

Aumento da velocidade: Ao carregar seletivamente os componentes necessários para a visualização atual e não carregar todos os componentes de uma vez, o carregamento preguiçoso de componentes pode aumentar a velocidade da aplicação.

Melhoria na experiência do usuário: Você pode melhorar a experiência do usuário informando que a aplicação está ativamente carregando conteúdo, utilizando Suspense para exibir uma indicação de carregamento.

Divisão de código: Uma das principais vantagens do lazy() é possibilitar a divisão de código. O processo de divisão de código envolve quebrar o código da sua aplicação em pacotes menores e sob demanda.

Isso minimiza o tamanho do pacote inicial e acelera o tempo de carregamento da sua aplicação.

Com lazy(), você pode realizar a divisão de código e o carregamento preguiçoso de componentes em seus aplicativos ReactJS.

Essa é uma ótima funcionalidade que se mostra útil para otimizar a eficiência e os tempos de carregamento das suas aplicações web, melhorando a experiência do usuário ao carregar componentes apenas quando necessário 🙃

Arquivos da lição

Os arquivos dessa lição podem ser encontrados neste repositório do GitHub.

Conclusão

E você, o que achou do <Suspense>, acha que vale a pena implementá-lo nos seus projetos?

Ou você acredita que fazer com gereciamento de estados é a melhor opção?

Até a próxima lição.