React: O conceito de React.Suspense

Uma analogia simples para entender essa funcionalidade!

Renderizando o lado esquerdo ou direito? — Créditos Jon Tyson

Como conversamos no meu artigo anterior, "React: O que é React.lazy e React.memo", a partir da versão 16.6.0 do React, já podemos utilizar React.Suspense em produção.

O prefixo unstable_* foi removido e foi dado a largada para utilização em massa (ou reescrita de projetos, afinal, Front-end né? 😬) dessa funcionalidade.

Ainda estamos descobrindo padrões e utilidades, assim mesmo como novos meios de planejar interfaces a partir desse novo momento na era de interfaces com React.

Mas qual a mágica por trás disso tudo? Vamos tentar explorar o conceito/idéia de como React.Suspense funciona.

Olhando React.Suspense mais de perto

[1] React.Suspense deixa você inserir efeitos colaterais (side-effects) declarativamente dentro de qualquer elemento na árvore de componentes (sejam eles classes ou funções) a partir do momento que você jogar uma Promise (throw Promise(...)) na sua pilha de chamadas atuais (call stack).

[2] A declaração throw irá delegar o controle de execução para o bloco catch mais próximo, caso não encontrado, o programa irá terminar. Você pode re-jogar uma excessão, dessa maneira, ela irá se propagar pela sua pilha de chamadas até o topo (primeira função executada).

Meio confuso? Vamos analisar alguns exemplos, começando com o item [2]:

// ~/Desktop/throw-e-stack.js
let fn1 = () => {
console.log("[FN1]");
fn2()
}
let fn2 = () => {
console.log("[FN2]");
fn3()
}
let fn3 = () => {
console.log("[FN3]");
fn4()
}
let fn4 = () => {
console.log("[FN4]");
throw "Valor da fn4";
}
let render = () => {
try {
fn1()
} catch (top) {
console.log(top);
}
}
render();

O resultado será algo assim:

$ node throw-e-stack.js
[FN1]
[FN2]
[FN3]
[FN4]
Valor da fn4

Como podemos ver, o valor jogado pela fn4 foi propagado até o topo na função render(). Tudo isso devido a não termos "alguém no meio" para capturar a excessão jogada.

E como isso funcionaria em um ambiente parecido com React? Vamos ilustrar de uma maneira BEM simples, o conceito que é o importante aqui:

// ~/Desktop/exemplo-render-suspense.js
setInterval(render, 1000);
render();
function render() { // [A]
let component = suspense(App, "Carregando...");
console.log("[RENDER]", component);
};
function suspense(children, fallback) { // [B]
try {
return children();
} catch (it) {
if (!it.done) {
return fallback;
}
}
};
function App() { // [C]
let res = createCache("livros", getBooks);
return res.map(item => `Livro ${item}`);
};
function getBooks() { // [D]
return new Promise((res, rej) => {
setTimeout(() => res([1, 2, 3]), 1000);
});
};
let cache = {}; // [E]
function createCache(id, promise) {
let it = cache[id];
//
// "record" já criado e "scope" sendo resolvido
//
if (it && it.done) { // [F]
return it.data;
}
  if (it && !it.done) { // [G]
throw it;
}
  //
// adicionando novo "record" ao cache e iniciando "scope"
//
let scope = promise(); // [H]
  let record = {
id,
thenable: scope,
done: false,
data: null
};
  record.thenable.then(res => { // [I]
record.done = true;
record.data = res;
});
  cache[id] = record;
  throw record; // [J]
}

Teremos como resultado:

$ node ~/Desktop/exemplo-render-suspense.js
[RENDER] Carregando...
[RENDER] Carregando...
[RENDER] [ 'Livro 1', 'Livro 2', 'Livro 3' ]
[RENDER] [ 'Livro 1', 'Livro 2', 'Livro 3' ]

Vale lembrar que a implementação do componente React.Suspense é bem complexa, o código é simplesmente uma analogia, para termos uma idéia de como tudo funciona.

A implementação do React é composta de 4 fases: ReactFiberBeginWork, ReactFiberUnwindWork, ReactFiberCompleteWork e ReactFiberCommitWork. Que estão fora do escopo desse artigo.

Vamos analisar os pontos marcamos no código acima:

  • [A]: A função para execução do nosso programa. Em JSX seria algo como: ReactDOM.render(<Suspense><App /></Suspense>, ...) . Temos um setInterval logo acima, para renderizar nossa aplicação a cada 1 segundo, simulando o ciclo de renderização do React.
  • [B]: Os nomes de parâmetros/prop ajudam um pouco, em JSX seria semelhante a: <Suspense fallback={}>...</Suspense> , nossa função suspense tenta executara função children dentro de uma declaração try...catch, caso alguma excessão seja retornada, o bloco catch terá controle da execução, recebendo um parâmetro it , caso o valor de .done não seja verdadeiro, estamos retornando o valor do fallback para quem chamou essa função (nesse exemplo, o item [A]).
  • [C]: Nosso componente App, que foi passado para função render do item [A]. No seu corpo, estamos passando uma função getBooks (que retorna uma Promise) para uma função chamada createCache (mais detalhes a seguir), ao retornar o valor para res estamos criando um array de strings com o resultado dessa operação.
  • [D]: A função getBooks retorna uma Promise , que será resolvida apenas depois de 1 segundo.
  • [E]: Nossa função createCache e um objeto global cache. Nessa função estamos recebendo um id único (para ser usado como chave do cache) e a Promise a ser executada. No início da função verificamos nosso cache para a existência do id passado e criamos let it = cache[id] .
  • [F]: Se o id existir em cache e a Promise já foi resolvida, retorne o valor de it.data . O valor retornado será recebido no item [C], onde chamamos a função let res = createCache("livros", ...).
  • [G]: Se o id existir em cache e a Promise ainda não foi resolvida, vamos jogar o objeto complete com throw it. Isso é necessário para executar o item [B], em outras palavras, o try...catch da nossa função suspense(...) onde verificamos por !it.done => fallback, retornando o fallback como resultado.
  • [H]: Executamos a Promise passada em promise, no exemplo getBooks, iniciando sua execução/carregamento.
  • [I]: Estamos chamando .then nessa Promise para que, ao término da sua execução, poderemos salvar os valores retornados em .data e atualizar seu estado .done para true (não estamos cuidando de casos de erro, ex: .catch, para simplificar o entendimento).
  • [J]: O mesmo conceito do item [G], estamos jogando o objeto completo para ser executado pelo item [B].

**phew, phew**… Quanta coisa! hahaha

No artigo anterior utilizamos React.lazy que, na sua implementação, cria uma clojure retornando null caso o componente não tenha sido carregado. Bem parecido com o createCache acima.

Finalizando

Interessante ter uma idéia de como toda a mecânica funciona. E não estamos levando em conta o agendamento de tarefas, diferença de elementos DOM ou fase de atualização e ciclos de vida.

Espero que isso te ajude a valorizar ainda mais alguns benefícios que temos hoje em dia e claro, entender melhor o que você está utilizando.

React.Suspense e ReactFiber* criaram um processo de agendamento de tarefas extremamente poderosos. Eu estou animado para explorar suas possibilidades!