Objeto: servir páginas pré-renderizadas com base no caminho para o recurso solicitado.
Principais termos técnicos abordados: NextJS, Pré-renderização, rota dinâmica, useRouter, getStaticPaths, getStaticProps, react hook, fetch, async, await, JavaScript, HTML
Requisitos para as instruções funcionarem: ter concluído a Receita6 e estar com o projeto resultante aberto
Requisitos para compreensão das instruções: noções básicas de programação, HTML e NextJS, bem como ter concluído textos anteriores
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
Suponha que queiramos escrever uma página para um filme em particular. Por exemplo o filme cujo identificador é tt0095801 na api do site que usamos como fonte de dados em nossas receitas (o www.omdbapi.com).
O nextjs tem funcionalidades de rotas dinâmicas e renderização estática que pode nos ajudar nisso.
Vamos lá.
Crie um diretório movies e, dentro dele, crie um arquivo [id].js (o nome é exatamente esse, com colchetes)
Esse id entre colchetes é a tal rota dinâmica. A gente taxa de dinâmica porque, na prática, se você tentar acessar /movies/dorianGray.js o next vai carregar seu arquivo [id].js e os scripts dentro dele poderão acessar o dorianDray através de uma variável (ou constante) id. O mesmo acontece para qualquer recurso que você solicitar dentro desse diretório, naturalmente. Por exemplo, se você acessar /movies/badgad79.js o valor bagdad79 estará disponível nos scripts de [id].js através de uma variável id.
- Tá bom, e qual é a vantagem disso?
Bem, ajuntando o conhecimento de outras receitas, a gente pode fazer uma página só, o [id].js, que inicialmente exibe uma mensagem "Carregando..." e que, internamente, obtém o valor do id na requisição, faz uma pesquisa por um filme específico na API e, quando a pesquisa retornar, substitui o "Carregando..." pelas informações do filme.
A gente precisa, para isso, do useSWR(), para carregar uma página e iniciar um processamento, no navegador, para fazer a pesquisa.
E a gente precisa do useRouter(), um "react hook", que vai ser usado para acessar o valor do id.
Escreva o seguinte código no [id].js
import useSWR from 'swr'
import { useRouter } from 'next/router'
export default function TheMovie(){
const {id} = useRouter().query
const {data, error} = useSWR(`https://www.omdbapi.com/?apikey=ME_SUBSTITUA&i=${id}`, async (u) => {
const res = await fetch(u)
const json = await res.json();
return json;
})
if (error) return <div>Erro na requisição/resposta </div>
if (!data) return <div>Carregando...</div>
if (data.Error) <div>Erro</div>
return (
<div>
<div>{data.Title} --- {data.Year}</div>
<div>{data.Plot}</div>
<div>
<img src={data.Poster} width="300" height="400"/>
</div>
</div>
)
}
Note o ME_SUBSTITUA acima. Você precisa substituir esse texto pela chave de API que você obteve nas receitas anteriores. Isso vale para todo o código dessa receita em que aparecer o termo ME_SUBSTITUA.
Salve o arquivo, inicie o sistema com npm run dev e acesse https://localhost:3000/movies/tt0095801
Você há de ver, inicialmente, uma mensagem "Carregando..." e, pouco tempo depois, algo parecido com isso...
Voltando ao código, vemos que não há nada de novo além da linha
const {id} = useRouter().query
Parece bem óbvio que ela vai "pegar" o id da página que tiver sido requisitada, o nome antes do ".js" no arquivo. Essa constante TEM que ser declarada como id, nesse caso, porque nosso arquivo se chama [id].js. Se nosso arquivo lá no diretório pages/movies se chamasse [cachorra].js teríamos que reescrever essa linha como const {cachorra} = useRouter().query
No restante do código, fazemos uma requisição a uma API, embutindo o id na chave de pesquisa na URL...
`https://www.omdbapi.com/?apikey=ME_SUBSTITUAe&i=${id}`
... e usamos o react hook useSWR() para fazer com que nossa função TheMovie seja chamada novamente tão logo cheguem dados da requisição ao omdbapi ou ocorra algum erro com ela. Se você não estiver entendendo essa parte, vá para as Receitas 5 e 6, pois nada desse código é novo para a gente, tirando o useRouter().
- Sim, e esse bicho é o que? Esse useRouter().
O useRouter() é um "react hook" que retorna um um objeto, um "roteador". Esse objeto pode ser usado para redirecionar para outra página, por exemplo. E pode ser usado também para acessarmos o identificador da página solicitada pelo navegador, como é o caso aqui. O objeto retornado por useRouter() tem uma propriedade query. Dentro dessa propriedade está o nosso id. Estamos usando desestruturação para pegar o id de dentro do query.
- Ah, beleza, entendi. Mas e o useSWR()? Quando a gente usa o useSWR() o código roda apenas no navegador, né não?
Né não. Mas esse é um bom ponto e precisa de uma explicação mais elaborada.
Se você escrever a linha...
console.log(`data=${data}`)
...logo antes do primeiro if no [id].js, e em seguida reiniciar o sistema com npm run dev, você há de ver no terminal de comando, entre várias linhas, essa aqui
data=undefined
Isso quer dizer que o next.js pré-renderizou sua página - preparou uma versão inicial dela, rodando a função TheMovie() no servidor e guardando seu resultado para quando alguém requisitá-la ele, o servidor, já ter alguma coisa pronta para mandar. Essa pré-renderização é, como a gente diria no sertão, "a moral" do next.js. O framework vai tentar fazer isso o tempo todo, sempre que puder. Nesse caso, se 1 milhão de usuários requisitarem essa página ao mesmo tempo, todos serão servidos com uma página HTML estática que foi pré-renderizada quando o sistema foi implantado.
Se quiser conferir se é assim mesmo, você pode desabilitar o JavaScript do seu navegador, acessar http://localhost:3000/movies/tt0095801 e visualizar o código-fonte da página. Seu <div>Carregando...</div> estará lá, junto com uma ruma de outras marcas que o next.js inclui. Opcionalmente, você pode tentar fazer "na tora" o que o navegador faz quando você digita o endereço da URL e dá enter. No Linux, isso pode ser feito no terminal pelo comando telnet localhost 3000. Isso abre uma conexão com o servidor da aplicação (se estiver no ar, claro). Em seguida, basta digitar GET /movies/tt0095801 (GET é um comando do protocolo HTTP, é mais das vezes o que o seu navegador conversa com o servidor Web) e dar enter duas vezes. Você deve receber uma resposta como a da imagem a seguir... Note o <div>Carregando</div> (deve dar um real de trabalho, mas você o acha) - é um código retornado na função TheMovie() que foi chamada no servidor.
- Sim, mas quando usamos o navegador, depois de ver esse "Carregando...", a gente viu uma página com a descrição do filme e o poster...
Pois é. Depois que a página pré-renderizada é exibida no navegador, por debaixo dos panos os componentes do React começam a ser carregados e vão dar aos diversos componentes a dinâmica que implementamos. Esse processo é chamado de hydration.
Nesse caso, se você escreveu o console.log() que eu pedi, pode inspecionar a página quando ela for carregada e vai ver algo desse tipo...
... isso significa que sua função TheMovies() foi chamada diversas vezes no navegador, além de ter sido chamada lá no servidor durante o processo de pré-renderização. Não é incrível?
- é sim... mas é um negócio meio sem futuro. Fosse para pré-renderizar, era melhor pré-renderizar a página já completa, com poster etc... Nesse caso aí 1 milhão de navegadores ainda irial moer um script para fazer uma requisição à API e montar a página. E todos montariam exatamente a mesma página.
Bem observado. E dá pra pré-renderizar a página completa, com descrição, e imagem em vez da mensagem "Carregando...". Isso evitaria "1 milhão" de fetch() a um mesmo recurso para processar e montar uma mesma página para esse 1 milhão de requisições.
Note apenas que, para montarmos as páginas HTML completinhas, de antemão, antes das requisições, precisamos de duas coisas.
Primeiro, precisamos nos livrar do useSWR(). Esse é um hook usado para renderização final no navegador. Queremos que os dados de um filme em particular - resultantes do fetch() - não mais sejam obtidos através de uma chamada ao useSWR() mas através de uma função que o next.js execute exclusivamente no servidor e quando da implantação do sistema. O next.js oferece a possibilidade de escrevermos esse tipo de função, ela precisa chamar-se getStaticProps() e precisa estar dentro de um arquivo js que contenha uma página, que contenha uma função exportada como default, em outras palavras. O objetivo do getStaticProps() é justamente ir buscar dados específicos e passá-los para essa função exportada como default para que ela construa a página no chamado build time - quando o sistema estiver sendo implantado.
Segundo, precisamos de uma outra função que nos retorne um vetor com todos os possíveis identificadores de filmes a serem usados para pré-renderizar as páginas. O next.js pode, assim, chamar essa função, obter todos os identificadores e, para cada um deles, chamar o getStaticProps() [passando o id como parâmetro] e, em seguida, chamar a função TheMovie() [passando os dados obtidos no getStaticProps() como parâmetro] e gerando, de antemão, todas as páginas para todos os identificadores de filmes. No next.js, essa função que retorna um vetor com os identificadores deve chamar-se getStaticPaths().
Vamos implementar esses dois pontos discutidos, começando pelo getStaticPaths(), que é mais simples.
Escreva a seguinte função ao fim do arquivo [id].js
export async function getStaticPaths(){
return {
paths:[
{params: {id: "tt0095801"}},
{params: {id: "tt0033152"}},
{params: {id: "tt0015400"}},
{params: {id: "tt0041149"}},
{params: {id: "tt0044388"}},
{params: {id: "tt0098746"}},
{params: {id: "tt0046322"}},
{params: {id: "tt0046497"}},
{params: {id: "tt0044389"}}
],
fallback: true
}
}
Notemos algumas coisas:
Quase tudo isso é burocracia e é coisa fixa - tem que ter export e async, o nome da função tem que ser esse e tem que retornar um objeto com dois atributos, paths e fallback. Ademais, o atributo paths tem que ser um vetor de objetos e cada objeto desse vetor deve conter um atributo params que é, também, um objeto. Tudo isso é fixo, errou uma letra, acanalhou tudo. Então prestem atenção na hora de usar. Por fim, nesse exemplo da gente, como o nome de nosso arquivo de rota dinâmica é [id].js, os objetos definidos para os params DEVEM ter um atributo id com valor de um identificador válido na API onde estamos indo buscar os dados de nossos filmes. Notem que estamos dentro de uma função rodando num servidor conectado à Internet. Podemos ir atrás desses id's em arquivos de texto, na própria API, num banco de dados relacional dentro dos nossos domínios, podemos deixar fixos, hardcoded, assim como deixamos, podemos ir buscar esses id's até no inferno se o satanás botar uma base de dados por lá e nos der os meios de acesso. Mas seja lá onde formos buscar esses id's, precisamos retorná-los respeitando a burocracia que estamos delineando aqui.
Bem, uma vez que sabemos obter os nossos identificadores, vamos escrever a função que sabe ir buscar dados para a página a partir de um identificador.
Acrescente a seguinte função ao fim do arquivo [id].js
export async function getStaticProps({ params }) {
const res = await fetch(`https://www.omdbapi.com/?apikey=f1cbc41e&i=${params.id}`)
const movie = await res.json();
return {
props: {
movie
}
}
}
Uma vez mais: nada que já não tenhamos feito antes. É um tanto "parecido" com o getServerSideProps() de receita anterior, no entanto, esse código do getStaticProps() roda quando o sistema é implantado e recebe informações que foram geradas no getStaticPaths(). O getServerSideProps() roda uma vez a cada requisição do navegador e recebe informações do próprio navegador. São coisas beeem distintas, por isso o "parecido" foi entre aspas. A única semelhança mesmo é que tanto o retorno do getServerSideProps() quanto o retorno do getStaticProps() serão passados como parâmetro para a função exportada como default - nesse caso, a função TheMovie(). Resta-nos, então, reescrever essa função.
Passo 6
Nossa função TheMovie() estava assim:
export default function TheMovie(){
const {id} = useRouter().query
const {data, error} = useSWR(`https://www.omdbapi.com/?apikey=f1cbc41e&i=${id}`, async (u) => {
const res = await fetch(u)
const json = await res.json();
return json;
})
if (error) return <div>Erro na requisição/resposta: {error}</div>
if (!data) return <div>Carregando...</div>
if (data.Error) <div>{data.Error}</div>
return (
<div>
<div>{data.Title} --- {data.Year}</div>
<div>{data.Plot}</div>
<div>
<img src={data.Poster} width="300" height="400"/>
</div>
</div>
)
}
Note que, com esse trabalho todo que tivemos (vá lá: nem tanto trabalho assim), muita coisa que estava sob a responsabilidade da função TheMovie(), como lidar com identificadores de rota dinâmica e carregar dados json através de um fetch() a uma API, tudo isso foi distribuído entre as outras duas funções que escrevemos. Assim, a função TheMovies(), como diria meu saudoso primo Juju, vai ficar só com "o milho da pipoca": vai receber os dados prontinhos e montar a página.
Reescreva a função TheMovie() e suprima os imports dos hooks useSWR() e useRouter() - não vamos precisar deles. O código completo deve ficar como a seguir...
export default function TheMovie({data}){
console.log(`Pré-renderizando ${data.Title}`)
return (
<div>
<div>{data.Title} --- {data.Year}</div>
<div>{data.Plot}</div>
<div>
<img src={data.Poster} width="300" height="400"/>
</div>
</div>
)
}
export async function getStaticPaths(){
return {
paths:[
{params: {id: "tt0095801"}},
{params: {id: "tt0033152"}},
{params: {id: "tt0015400"}},
{params: {id: "tt0041149"}},
{params: {id: "tt0044388"}},
{params: {id: "tt0098746"}},
{params: {id: "tt0046322"}},
{params: {id: "tt0046497"}},
{params: {id: "tt0044389"}}
],
fallback: true
}
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://www.omdbapi.com/?apikey=f1cbc41e&i=${params.id}`)
const data = await res.json();
return {
props: {
data
}
}
}
E, basicamente, é isso. Seu navegador deve receber uma página HTML já montadinha, construída no servidor, antecipadamente à sua requisição.
Mas como diz minha Veinha, "desengano da vista é ver". Para saber se recebemos realmente a página HTML pronta, vamos fazer aquele mesmo telnet que fizemos anteriormente. O resultado deve ser algo como a figura seguinte...
Note que os div's retornados pela função TheMovie() estão lá.
Um derradeiro lembrete: em ambiente de desenvolvimento, o next.js vai gerar a página a cada requisição, mesmo quando a gente usa o getStaticProps(). Eu deixei intencionalmente um console.log() na função para você verificar isso. Em modo de desenvolvimento, você deve ver o que aquele console.log cospe no terminal a cada requisição, muito embora isso seja programado para ser gerado no build. Ponha o sistema em modo de produção e você há de ver uma ruma de log's gerados por aquela chamada.
- Só mais uma, professor: e aquele fallback: true no retorno do getStaticProps(), quer dizer o que?
Ainda vai ser um assunto a ser explorado mais profundamente. Nesse caso específico, quer dizer que, se a gente requisitar um caminho com um id que não esteja no retorno do getStaticPaths(), o next.js vai nos dar a possibilidade de, com poucas linhas de código, funcionar como no início dessa receita: botar uma mensagem "Carregando..." enquanto a página solicitada fora das previstas na pré-renderização está sendo montada. O bom é que isso acontece apenas na primeira requisição. O next.js salva a renderização completa para requisições posteriores - vai "aprendendo" quais são os identificadores e que páginas elem montam, em outras palavras.
- Beleza, mostra aí o código como fica, então...
Nada. Essa vai de exercício :P
Rotas dinâmicas: https://nextjs.org/learn/basics/dynamic-routes
Pré-rendering: https://nextjs.org/learn/basics/data-fetching
Montar essa mesma receita, mas fazendo uma requisição a uma api de sua preferência.
Bonitizar a página com elementos visuais mais aprazíveis aos olhos do usuário
Se você tentar acessar https://localhost:3000/movies/tt0029833, deve encontrar uma mensagem de erro parecida com: "Server Error TypeError: Cannot read property 'Title' of undefined". O identificador tt0029833 é válido na API que estamos usando, mas isso acontece porque o tt0029833 não está entre os identificadores retornados pelo getStaticPaths() o que, em consequência, faz com que não seja feita nenhuma consulta com esse id no getStaticProps() e o parâmetro data, passado ao TheMovie({data}) termina indo como undefined (o null do JavaScript). Quando a gente tenta fazer o console.log(data.Title), dá esse erro, pois um objeto undefined não tem uma propriedade Title, obviamente. Tirar o console.log() não resolve, pois há mais referências a propriedades do objeto data no return. Conserte esse comportamento indesejado SEM mexer no getStaticPaths(). Para identificadores não previstos, o sistema deve por uma página simples com a mensagem "Carregando...". Se você complicar a solução, ela estará provavelmente errada. Eu resolvi por aqui acrescentando uma linha de código. Umazinha.
Mesmo resolvendo a parada no exercício anterior, nossa interface não deve ficar legal SE requisitarmos um id que NÃO esteja disponível na API de onde estamos puxando nossos dados. Se acessarmos https://localhost:3000/movies/xxxccc, por exemplo, o ideal seria que exibíssemos uma mensagem indicando que não há o recurso solicitado no sistema. Implemente isso.