Objeto: criação de uma página que é construída com base em interação com com um sistema externo. A interação com o sistema externo é realizada no cliente/navegador, ou no "front-end", nome mais moderninho. Utilizaremos a biblioteca SWR no processo.
Principais termos técnicos abordados: NextJS, SWR, react hook, fetch, async, await, getServerSideProps, JavaScript, HTML
Requisitos para as instruções funcionarem: ter concluído a Receita3 e estar com o projeto resultante aberto
Requisitos para compreensão das instruções: noções bem básicas de programação, HTML e NextJS
Como ler essa receita: instruções, dentro dos passos da receita e que requerem ação sua no computador, estarão escritas em cor azul. Comentários sobre os passos estarão em fonte normal, cor preta. Comandos, código-fonte, termos técnicos ou configuração explícita, estarão com fonte diferenciada.
Observação: eventuais instruções de terminal serão comandos Linux. Adapte caso esteja usando outro sistema operacional
Vamos fazer uma aplicação Web que vai interagir com um sistema externo. Nosso software vai comunicar-se com esse sistema, via http, fazendo-lhe alguma solicitação. O sistema vai responder com um texto JSON, um texto estruturado facilmente manipulável em JavaScript. Nosso aplicativo usará essa resposta para montar uma página HTML com os dados recebidos do sistema externo.
Diferentemente do que fizemos na Receita3, onde o código JavaScript que interage com o sistema externo roda no servidor, a cada requisição de página, dentro da função getServerSideProps(), aqui o código JavaScript que interage com o sistema externo roda no cliente, no navegador.
Todos os outros aspectos são essencialmente iguais aos da Receita3.
Vou reescrevê-los aqui, em todo caso
Esse sistema externo pode ser o de qualquer empresa, inclusive a NASA. Isso mesmo, a NASA e outros entes gigantescos disponibilizam a execução de funções EM SEUS SERVIDORES a partir de requisições http (ou https) e essas funções retornam, através do mesmo protocolo, um texto estruturado.
As respostas para as requisições, ou o "retorno das funções" executadas por esses sistemas externos, vêm no tal formato JSON e não em HTML como seria de se esperar. Os programas que recebem os dados no formato JSON, no entanto, podem processar esses dados para, entre outras coisas, construírem páginas HTML para serem vistas por viventes.
A gente pode entender esse cenário como sendo uma máquina (uma página do nosso aplicativo Web, num navegador) solicitando a execução de uma função numa outra máquina (o sistema externo) e processando o resultado da execução dessa função pelo sistema externo (um texto JSON) para construir algo intelegível para algum usuário (uma página HTML).
A gente chama esse conjunto de funções disponibilizadas para execução por esse sistema externo de API. Não esqueça esse nomezinho, ele é importante. No mundo Web, API é como um sistema para ser acessado por um software e não por uma pessoa. Você quer oferecer em seu aplicativo o login através do Google ou do Facebook, por exemplo? Como você não é nem o Google nem o Facebook, torça para que essas empresas disponibilizem em seus servidores uma API para que o seu aplicativo possa interagir com os sistemas de Google e Facebook e chamar alguma função de autenticação.
Essas API's normalmente não estão abertas para qualquer programa solicitar a execução de suas funções. As empresas que hospedam as API's comummente fazem algumas restrições para não acanalhar o tráfego em seus servidores.
Este sistema que acessaremos especificamente nesta receita exige que cada requisição que lhe é submetida contenha uma "API key", uma espécie de passe ou autorização para que a requisição seja efetuada. Se você não tem essa API key, remeta-se à receita anterior.
Abrir o projeto resultante da Receita3 em seu ambiente de desenvolvimento preferido.
Criar um novo arquivo, movies2.js, no diretório pages.
Adicionar o seguinte conteúdo ao arquivo:
async function fetcher(url) {
const res = await fetch(url)
const json = await res.json()
return json
}
Notem: essa função não é exportada, tampouco por default. Não é, portanto, nem uma página HTML associada a uma rota nem uma marca HTML que possa ser usada numa página. É uma função JavaScript simples. Estará disponível para ser chamada pelas funções da página que haveremos de construir em breve.
- E o que ela faz?
Primeiro, conecta-se com uma url, recebida como parâmetro, através da linha...
const res = await fetch(url)
A url é o endereço remoto do recurso que desejamos acessar, com parâmetros e tudo.
O fetch() é a mesma função que utilizamos para fazer esse mesmo serviço executando do lado do servidor.
- E esse await?
Esse await está escrito aí porque o fetch() é um método assíncrono. Ele retorna imediatamente, antes mesmo de ter uma resposta para a requisição. O uso do await indica que seu algoritmo vai esperar ali até que chegue alguma resposta efetiva da chamada do fecth() - a resposta pode ser um erro, inclusive.
A outra linha...
const json = await res.json()
...transforma a resposta recebida pelo fetch() em JSON e a retorna. O await é usado pelo mesmo motivo que o usamos antes, no fetch(): o método json() é assíncrono e retorna antes mesmo da conversão estar pronta.
- e aquele "async" lá no cabeçalho da função?
Bem, toda função que usa a palavra-chave await deve ser declarada como async - assíncrona. Se vocês estiver usando um ambiente de desenvolvimento de vergonha e esquecer o async, você deve receber algum tipo de mensagem sinalizando isso.
Por enquanto temos apenas uma função JavaScript que solicita um recurso a um sistema externo e processa a resposta, transformando-a num objeto JSON. Teoricamente, bastaria reescrevermos o arquivo movies2.js como se segue, substituindo o texto ME_SUBSTITUA pela chave que você obteve na receita anterior:
export default function Movies2(){
const data = fetcher(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`)
return (
<div>
{ data.Search.map( (m) => <div>{m.Title} --- {m.Year}</div> ) }
</div>
)
}
async function fetcher(url) {
const res = await fetch(url);
const json = await res.json();
return json;
}
Mas não nos animemos, isso não deve funcionar. Você pode tentar, por curiosidade, se assim desejar. Mas fetcher() é função assíncrona e retorna imediatamente, logo o return de Movies2() não faz sentido. A função Movies2(), ademais, é uma página (export default...), não pode ser função assíncrona.
- Ah, não seria legal se nessa página a gente pudesse retornar algo como "carregando" e, quando os dados efetivamente estivessem disponíveis, tirar o "carregando" e exibir os dados?
Seria sim, seria muito interessante, e é absurdamente fácil.
Isso é possível através da utilização de hooks (ganchos, em tradução literal).
Hooks são funções especiais de uma biblioteca JavaScript, o React, funções que nos permitem implementar coisas como manter o estado de uma página, o que não é o caso aqui mas é o caso mais conhecido. Não vamos entrar muito em detalhes sobre hooks agora, mas vamos usar um deles para fazer o que queremos fazer.
Primeiramente, precisamos importar a biblioteca swr, que contem a função useSWR, que é o tal hook que queremos usar.
Então importe...
npm i swr
Agora precisamos usar o hook.
Então deixe o movies2.js com o seguinte conteúdo:
import useSWR from 'swr'
export default function Movies2(){
const {data, error} = useSWR(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`, fetcher)
if (error) return <div>falha na requisição...</div>
if (!data) return <div>carregando...</div>
return (
<div>
{ data.Search.map( (m) => <div>{m.Title} --- {m.Year}</div> ) }
</div>
)
}
async function fetcher(url) {
const res = await fetch(url);
const json = await res.json();
return json;
}
Agora deve funcionar.
Salvando o arquivo e acessando http://localhost:3000/movies2 você deve ver, primeiro, a mensagem "carregando..." e, algum tempo depois, essa mensagem deve ser substituída por uma lista de filmes.
Vamos entender o que está acontecendo.
Primeiro, um hook é uma função de alguma biblioteca. Nesse caso, o hook é a função useSWR() e a biblioteca, pacote ou como você queira chamar é swr, desenvolvido pela Vercell. Precisamos importar o hook como qualquer outra função que não esteja disponível por default (como a função fetch, que invocamos dentro de fetcher sem precisar importá-la):
import useSWR from 'swr'
Isso nos permite chamar a função useSWR(). Estamos fazendo isso já na primeira linha de nossa função Movies2().
const {data, error} = useSWR(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`, fetcher)
- E o que djabo essa linha faz?
Vamos responder informalmente. Precisamos apenas lembrar que nossa função Movies2() é chamada por componentes que nós não escrevemos. A ideia dos hooks é que a gente propague alguma ação para além da execução de uma função. Nesse caso do useSWR() dentro do Movies2(), funciona como se o useSWR() fizesse algum serviço e ainda dissesse para esses componentes que executam a função Movies2(): "ei, ruma de macho, se acontecer alguma novidade com esses parâmetros aqui, chame essa função Movies2() novamente".
- e o que é exatamente esse serviço que o useSWR() faz?
Ele chama a função assíncrona que recebe como parâmetro, a função fetcher(), repassando para esta função fetcher() a url que useSWR() também recebe como parâmetro que, nesse caso, tem o valor fixo de `http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`. O useSWR() retorna imediatamente um objeto com os atributos data e error enquanto o fetcher() está fazendo seu serviço de conectar-se com uma máquina remota, solicitar-lhe um recurso, receber a resposta e transformá-la num JSON (convenhamos, demora um pouco).
Vamos a um breve parêntese sobre JavaScript. Esse tipo de atribuição...
const {data, error} = useSWR(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`, fetcher)
... é chamada de atribuição via desestruturação. Poderíamos fazer simplesmente...
const resultado = useSWR(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`, fetcher)
... e, nos ifs, usarmos if (resultado.error) e if (!resultado.data)
Mas a atribuição por desestruturação nos permite já "apartar" os atributos do objeto resultante em duas variáveis distintas, data e error. A desestruturação é extremamente comum em exemplos e vídeos e tutoriais na Internet. Você pode gostar mais da segunda alternativa, mas tem que se acostumar com a desestruturação.
Fim do parêntese sobre JavaScript, voltamos à receita, ainda com uma explicação informal do que acontece.
Quando acessamos a página http://localhost:3000/movies2 pelo navegador, os tais componentes chamam a função Movies2() pela primeira vez. E, por conseguinte, a função Movies2(), também pela primeira, chama o hook useSWR(). O useSWR() retorna imediatamente um objeto com os atributos data e error nulos, mas faz a chamada ao fetcher() e, não esqueça: os componentes que invocaram o Movies2() ficam "ligados" no resultado dessa função fetcher().
A execução segue e o primeiro if...
if (error) return <div>falha na requisição...</div>
... não executa seu corpo (o JavaScript entende null como false)
O segundo if, por sua vez...
if (!data) return <div>carregando...</div>
... executa seu corpo (a negação do null é entendida como true no JavaScript) e a função Movies2() retorna o <div>carregando...</div>, que vai ser incorporado ao HTML final da página pelos componentes que chamaram o Movies2().
Mas lembrem, crianças, enquanto isso acontece o fetcher() invocado de dentro do useSWR() está 'moendo' para realizar seu serviço. Quando ele finalmente retornar, os componentes que invocaram nossa função Movies2() vão invocá-la novamente para renderizar novamente a página. Nesta segunda execução, a linha...
const {data, error} = useSWR(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=bagdad`, fetcher)
...vai retornar diretamente a resposta que já está pronta. Se você não fez nenhuma besteira, como por a API key errada ou o endereço do servidor errado, e se nenhum contratempo aconteceu com o servidor que recebeu a requisição, o atributo data retornado não será nulo, terá um objeto JSON com os dados do resultado da requisição (igualzinho ao que aconteceu na receita anterior). Se algo deu errado, o data deve vir nulo e o error deve vir com alguma mensagem.
Em o valor data não sendo nulo e o valor de error o sendo, será executado...
return (
<div>
{ data.Search.map( (m) => <div>{m.Title} --- {m.Year}</div> ) }
</div>
)
... e os componentes responsáveis por renderizar a página substituirão aquele <div>carregando...</div> pelo resultado dessa execução.
Como vemos, muita coisa aconteceu, para muito pouco código que escrevemos.
Como não tem cerveja por conta da casa no bar da programação, isso significa que algum programador mais sabido que nós escreveu o código que a gente não teve que escrever. O useSWR(), inclusive, faz outras coisas legais, como cache de resultados, tornando a experiência do usuário mais agradável. Esse "swr" significa "stale while revalidate". Quer dizer, em linhas gerais, que quando você requisitar um recurso pela segunda vez, o useSRW() vai retornar de imediato a última versão do recurso (o usuário não vai ficar esperando uma nova requisição) acessada pelo próprio useSWR() e, por debaixo dos panos, vai buscar uma versão mais recente.
Tudo isso é ótimo, mas nos deixa na incumbência de aprender o esquema que esse programador mais sabido definiu para que as coisas acontecessem - o que também não é tão complicado, basta chamar o hook, nesse caso.
Por fim, cabe ressaltar que esse é um exemplo didático e que, como esse código roda no cliente, a API key vai ficar exposta para o mundo inteiro ver, copiar, colar e usar em seus próprios projetos.
Nesse caso específico, a ideia era aprender a fazer a interação com um servidor web a partir do cliente, é algo que será certamente útil, podemos interagir com nosso próprio sistema, como veremos mais adiante, sem necessidade de API key.
Mas se a interação envolver informações sensíveis, a melhor alternativa seria que esse código rodasse no servidor.
Há uma pesquisa por identificador no site www.omdbapi.com. Na página/função Movies2(), transforme os elementos em links para uma página específica onde são exibidas todas as informações de um filme em particular, inclusive a imagem de seu poster. Esse link deve se comunicar com o site externo para obter os dados específicos do filme. Essa comunicação deve ser feita do lado do cliente, como nesta receita.
Acessando a URL https://extreme-ip-lookup.com/json temos como resposta um objeto JSON com diversas informações sobre a máquina que acessou a URL, entre elas latitude, longitude, cidade e endereço IP. Faça uma página que exiba essas informações, não no formato JSON, mas em HTML. Perceba que, nesse caso, obrigatoriamente, temos que realizar a requisição do cliente, para que tenhamos informações da máquina onde roda o navegador.
Adapte o exercício 1 para que a mesma funcionalidade seja implementada, mas com a interação com o site sendo realizada do lado do servidor. Use uma outra página para isso (diferente da do Exercício 1), não misture as abordagens num mesmo arquivo js neste exercício.
Documentação oficial do hook: https://swr.vercel.app/
Texto mais resumido sobre hooks em geral: https://pt-br.reactjs.org/docs/hooks-overview.html
Vídeo em português sobre swr: https://www.youtube.com/watch?v=Pbs1VIwPoRA
Razões dos hooks existirem (opcional e excecivamente longo): https://pt-br.reactjs.org/docs/hooks-intro.html
Atribuição via desestruturação: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
A função map do JavaScript (e outras mais): https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Guide/Functions