React: Fetch API, React.Suspense e react-cache

Um jeito declarativo de chamar recursos externos!

Painel da React Conf 2018 — Créditos Code.FB

No artigo anterior, conversamos sobre uma analogia do que é o React.Suspense. Todo esse complexo sistema de agendamento de tarefas (scheduler) é o coração das novas funcionalidades do React 16.6+.

Até o momento, não há outras funções/ferramentas compatíveis com React.Suspense a não ser React.lazy para divisão de código. Se você estiver pensando "…já vou jogar meu fetch no React.Suspense…", infelizmente isso não irá funcionar por padrão.

Dan Abramov comentando que atualmente Suspense está limitado a divisão de código e não há nenhuma solução para data-fetching compatível no momento.

Depois de entendermos que jogar uma Promise,throw Promise(...), de qualquer elemento na árvore de componentes fará com que o React.Suspense se encarregue de agendar a atualização da minha aplicação, acredito que podemos explorar um pouco mais as opções atuais.

Vamos tentar?

Iniciando nosso exemplo

Nada mais prático do que iniciar com algum código:

$ mkdir react-fetch-cache
$ cd react-fetch-cache
$ npm init -y
$ npm install -S react react-dom parcel-bundler

Como você pode perceber, estamos utilizando a versão estável do React.

Vamos criar alguns arquivos:

$ touch index.html
$ touch mains.js

Em index.html podemos colocar:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>React</title>
</head>
<body>
<div id="root"></div>
<script src="./mains.js"></script>
</body>
</html>

E em main.js colocamos:

import * as React from "react";
import * as ReactDOM from "react-dom";
function App() {
return <h1>Hello</h1>;
}
ReactDOM.render(
<App />,
document.getElementById("root")
);

E podemos iniciar o Parcel:

$ ./node_modules/.bin/parcel index.html --no-cache

Isso irá rodar o Parcel em http://localhost:1234, nada muito novo por aqui, porém, vamos pegar aquela idéia de jogar uma Promise de algum componente?

Vamos utilizar API pública da Dog.ceo para buscar um JSON com uma URL de uma imagem de cachorro. Nosso main.js ficará da seguinte maneira:

import * as React from "react";
import * as ReactDOM from "react-dom";
function App() {
return (
<div>
<h1>Hello</h1>
<React.Suspense fallback={<span>Carregando...</span>}>
<Cachorro id="meu-cachorro" />
</React.Suspense>
</div>
);
}
let cache = {};
function Cachorro(props) {
let it = cache[props.id];
  if (it && it.done) {
return <img src={it.data} />;
}
  if (it && !it.done) {
throw it.thenable;
}
  let record = {
data: null,
done: false,
thenable: null
};
  record.thenable = fetch("https://dog.ceo/api/breeds/image/random")
.then(res => res.json())
.then(data => {
record.done = true;
record.data = data.message;
})
.catch(err => {
throw err;
});
  cache[props.id] = record;
  throw record.thenable;
}
ReactDOM.render(<App />, document.getElementById("root"));

NOTA: Se a estrutura acima está confusa para você, recomendo ler o meu artigo anterior "React: O conceito de React.Suspense".

Colocando uma GIF para ajudar a visualizar:

NOTA: Perceba que temos um "gap" em branco após o "Carregando…" sumir. Esse é o tempo em que a tag <img src={...} /> está levando para buscar e renderizar a imagem. Iremos falar disso mais a frente.

Dessa maneira, conseguimos integrar a versão atual do React e React.Suspense com um data-fetching declarativo e diretamente no nosso componente.

Mas cá entre nós, não quero ficar repetindo aquele gigantesco boilerplate para buscar dados de algo serviço. Imagine ter que manter, validar e invalidar o objeto cache, realmente não é algo viável.

Felizmente, temos uma maneira mais eficaz, que ainda está em fase experimental, chamada de react-cache.

Utilizando react-cache

O pacote react-cache é um pacote em fase experimental que e é compatível com React.Suspense para executar recursos externos (side-effects) a aplicação e utilizar as funcionalidades do React.Suspense enquanto processa o pedido.

A idéia acima do elemento it é um pouco (bem pouco e ridiculamente mais simples) baseada no ReactCache.js.

Agora que entendemos um pouco melhor para o que sever, vamos instalar essa dependência:

$ npm install -S react-cache

E atualizar nosso main.js para:

import * as React from "react";
import * as ReactDOM from "react-dom";
import { unstable_createResource } from "react-cache";
function App() {
return (
<div>
<h1>Hello</h1>
<React.Suspense fallback={<span>Carregando...</span>}>
<Cachorro id="meu-cachorro" />
</React.Suspense>
</div>
);
}
let CachorroResource = unstable_createResource(() => {
return fetch("https://dog.ceo/api/breeds/image/random")
.then(res => res.json())
.then(data => data.message);
});
function Cachorro(props) {
let url = CachorroResource.read(props.id);
return <img src={url} />;
}
ReactDOM.render(<App />, document.getElementById("root"));

Bem mais simples, não? 🤯 react-cache está isolando/abstraindo todo aquele complexo sistema de identificar se a Promise ainda está pendente, resolvida ou retornou um erro.

Não vou colocar outra GIF aqui, então você vai ter que acreditar em mim. Funciona, eu garanto! 😁

Mas mesmo usando react-cache, temos um "gap" em branco após o "Carregando…" sumir. Esse é o tempo em que a tag <img src={...} /> está levando para buscar e renderizar a imagem.

Será que podemos pedir ao React.Suspense para "esperar" até que a imagem seja carregada para só então remover o "Carregando…"?

Ficou na dúvida? A resposta é sim!

Carregamento de imagens para o react-cache

A tag<img src={...} /> realiza uma requisição HTTP normal. Sabendo disso, podemos utilizar sua API JavaScript para fazer seu carregamento e utilizar o react-cache para criar uma compatibilidade com React.Suspense.

Vamos direto ao código que pode ser mais fácil:

import * as React from "react";
import * as ReactDOM from "react-dom";
import { unstable_createResource } from "react-cache";
function App() {
return (
<div>
<h1>Hello</h1>
<React.Suspense fallback={<span>Carregando...</span>}>
<Cachorro id="meu-cachorro" />
</React.Suspense>
</div>
);
}
let CachorroResource = unstable_createResource(() => {
return fetch("https://dog.ceo/api/breeds/image/random")
.then(res => res.json())
.then(data => data.message);
});
function Cachorro(props) {
let url = CachorroResource.read(props.id);
return <Imagem src={url} />;
}
let ImagemResource = unstable_createResource(url => {
return new Promise((res, rej) => {
let img = new Image();
img.src = url;
img.onload = res;
img.onerror = rej;
});
});
function Imagem(props) {
ImagemResource.read(props.src);
return <img {...props} />;
}
ReactDOM.render(<App />, document.getElementById("root"));

Criamos um ImagemResource e um componente Imagem. Estamos passando a url da imagem como parâmetro para ImagemResource.read (que também será usada como chave para o cache interno do react-cache) e dentro do nosso componente Imagem estamos apenas executando a função .read (diferente do componente Cachorro no qual esperamos uma resposta do fetch).

Em ImagemResource estamos criando uma nova imagem com new Image() (que é equivalente há document.createElement('img')) e ao ser completamente carregada, estamos resolvendo a Promise em volta dessa imagem em .onload = res. Nesse momento, React.Suspense será notificado que esse elemento filho está pronto para ser resumido/renderizado.

Dessa maneira, temos 2 elementos dentro de App que está "jogando uma Promise" e React.Suspense irá cuidar de ambos, mostrando nosso fallback enquanto resolve os elementos filhos.

E agora uma GIF para mostrar como ficou:

Perceba que não temos mais o "gap" em branco, vamos diretamente de "Carregando…" (nosso fallback), para a UI completa (imagem devidamente carregada).

Finalizando

Vale lembrar que, para cuidar do estado de erro (caso alguma dessas Promise forem rejeitadas) precisamos utilizar o componentDidCatch em algum lugar da nossa árvore de componentes. Aí eu te pergunto, você já utilizou esse ciclo de vida?

O complexo sistema de agendamento de tarefas do ReactFiber e React.Suspense é incrível e teremos boas inovações nessa área. Eu estou bem animado!

Como Guillermo Rauch disse a dois anos atrás:

React é uma ideia tão boa que passaremos o resto da década continuando a explorar suas implicações e aplicações. — Guillermo Rauch.

Eu tenho certeza que com essas funcionalidades e outras que estão por vir para o React 17, pode adicionar mais outra década aí! 😁🎉