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, Static Site Generation (SSG), generateStaticParams, 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 entender o que é rota dinâmica agora para entendermos o que é geração estática mais adiante.
A ideia é que este identificador do filme esteja no caminho da rota, em vez de nos parâmetros da requisição. Ou seja, na hora de montar a URL para solicitar ao servidor, a gente vai ter algo como http://localhost:3000/movie/tt0095801 em vez de algo como http://localhost:3000/movie/?id=tt0095801.
Parece até bobagem, mas a primeira alternativa é mais interessante para que as "search engines" façam cache de sua página.
A grande questão é que não dá pra gente criar uma rota (um diretório com um page.js dentro dele) no nosso projeto para cada identificador de filme na nossa base de dados/API.
A gente precisa de uma rota dinâmica, onde esse identificador seja uma variável e não um valor fixo.
Vamos lá.
Crie uma rota /app/movie/[id]/page.js.
Você já deve ter percebido que esse id entre colchetes vai representar uma rota dinâmica. Se alguém acessar a rota http://localhost:3000/movie/tt0095801, esse id vai assumir o valor tt0095801 e vai poder ser acessado lá no page.js para influenciar na construção da página.
Vamos fazer isso.
Edite o page.js...
import React from 'react';
export default async function Home({ params }) {
const {id} = await params
const movie = await fetch(`https://www.omdbapi.com/?apikey=ME_SUBSTITUA&i=${id}`).then(res => res.json())
return (
<div>
<h1>{movie.Title} ({movie.Year})</h1>
<p>Director: {movie.Director}</p>
<p>Plot: {movie.Plot}</p>
</div>
);
};
Isso não tem quase nada de novidade para nós. Acessamos o id, que foi recebido dentro do parâmetro params (que é um Promise, por isso usamos o await), montamos uma pesquisa via fetch com o id para ir buscar os dados do filme e, com os dados do filme no formato JSON, montamos nossa interface gráfica.
Você precisa se ligar nessa parte burocrática e entender o que você tem liberdade de fazer e o que não tem.
Primeiro, o nome do parâmetro que representa a rota é params, e tem que aparecer desestruturado, entre chaves, na declaração da função: Home( { params } ).
Segundo, esse params é uma promessa, a partir de versão não sei das quantas, então a gente tem que empurrar um await para acessar o valor que nos interessa: const {id} = await params
Terceiro, o nome entre colchetes no diretório pode ser qualquer um. A gente chamou de id, mas se você quiser chamar sua rota dinâmica de cachorra, basta nomear o diretório [cachorra].
Apenas perceba que o nome entre colchetes tem que bater com a linha de código no page.js que vai recuperar o valor da rota dinâmica. Então, se a gente chamou de [id] o diretório, escrevemos const {id} = await params lá na função Home. Se a gente chamou de [cachorra] o diretório, escrevemos const {cachorra} = await params lá na função Home. Qualquer diferençazinha vai dar erro, inclusive com letra maiúscula e minúscula.
Isso tudo é beeem parecido com o que fizemos algumas receitas atrás, quando estudamos data fecthing a partir de um "server component" que recebia parâmetros de requisição. Não custa nada lembrar como era o código lá...
export default async function Home({searchParams}){
const {titleSearchKey = 'bagdad'} = await searchParams
const res = await fetch(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=${titleSearchKey}`)
const data = await res.json()
return (
<div>
<div>
{data.Search.map( (m) => <div key={m.imdbID}>{m.Title} --- {m.Year}</div> )}
</div>
</div>
)
}
Em síntese, a única diferença é que parâmetros de requisição estarão dentro de um objeto identificado por searchParams (que também é um Promise), enquanto parâmetros de rota estarão dentro de um objeto identificado por params. O resto é ir buscar dados com fetch e montar a interface gráfica com o JSON que vem na resposta. Note que, no código da receita anterior, usamos duas linhas com 2 await para fazer o fetch e a conversão em JSON...
const res = await fetch(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=${titleSearchKey}`)
const data = await res.json()
Já no código desta receita, conseguimos fazer a mesma coisa numa linha só, usando uma abordagem híbrida de then com await...
const movie = await fetch(`https://www.omdbapi.com/?apikey=ME_SUBSTITUA&i=${id}`).then(res => res.json())
Eu prefiro a abordagem desta receita, você pode usar a abordagem que achar melhor.
Mas tem que saber as duas.
Finalmente, salve o arquivo e acesse http://localhost:3000/movie/tt0095801
Você deve ver algo como...
- Só isso, professor??
Na verdade, não. Mas o restante também não é tão difícil.
A gente finalizou a questão da rota dinâmica, mas ainda falta a geração estática. Antes de chegar lá, vamos introduzir outro assunto interessante, que é o streaming de interface gráfica - a capacidade de seu app ir servindo pedaços de interface gráfica ao seu navegador conforme esses pedações estejam disponíveis, evitando que o usuário espere muito tempo para ver a interface toda de uma lapada só.
A gente pode mandar uma página simples com uma barra circular de carregamento logo em seguida à requisição, por exemplo, ou uma simples mensagem "Loading...", e quando a computação da página estiver pronta, a gente manda o resultado final para ser renderizado no lugar da mensagem "Loading..." ou da barra de progresso. Isso é o tal streaming de interface gráfica.
- E por que a gente quer isso, nesse caso?
Porque a gente está carregando a página quando o usuário faz a requisição. Quando a requisição chega ao nosso componente React (a função Home), a gente ainda vai fazer um fetch junto a um sistema externo (a API), antes de montar a interface HTML e enviá-la ao navegador.
Esse tipo de cenário pode levar a algum retardo na resposta - a gente não vê bem isso porque estamos tratando de requisições pontuais e pouca informação para montar a página. Mas devemos considerar o cenário como adequado para fazer streaming de intercace gráfica.
Vamos a isso.
Crie o arquivo /app/movie/[id]/loading.js com o seguinte conteúdo...
export default function Loading(){
return (
<div>
<h1>Loading...</h1>
</div>
)
}
Agora salve e abra um link com um identificador válido, por exemplo: http://localhost:3000/movie/tt0100405
Você deve ver a algo como...
...antes de ver a página devidamente carregada.
Isso é um exemplo de streaming no contexto das interfaces gráficas. Como a nossa página da rota /movie/[id]/page.js não está pronta na hora da requisição, o next faz o streamning imediato da página no loading.js. Quando a página do page.js estiver pronta, é feito o streamig dela automaticamente, e a substituição do conteúdo no navegador também é automática.
A moral da história é não deixar o usuário esperando, mas há outras vantagens em usar esse modelo de streaming para ir renderizando sua página progressivamente:
- qualquer componente, menu nos arquivos, links etc. nos arquivos layout.js da rota e acima da rota estarão visíveis e responsivos enquanto a página estiver sendo processada
- Você pode acionar qualquer desses links e seu app não vai esperar chegarem os dados da requisição anterior para processar a nova ação
Também é possível implementar streaming de dentro do próprio componente Home, sem usar o arquivo layout.js, através do (novo, enquento escrevo isso) componente <Suspense>, do React.
Mas isso é assunto para outra receita, porque a gente precisa fazer a tal geração estática.
Se a gente refletir um pouco vai perceber que os dados de cada um desses filmes quase nunca mudam ou realmente nunca mudam: se daqui a um ano você fizer outra requisição com esse mesmo id que estamos usando aqui vai ver precisamente os mesmos dados.
Isso é bem comum no desenvolvimento web, por mais que tenhamos páginas dinâmicas, base de dados etc.
Uma notícia num portal de notícias raramente muda (o que muda são os comentários);
A descrição de um produto num site de e-commerce raramente muda (o preço é que pode mudar com alguma frequência);
A landing page de um sistema acadêmico (depois que você entra com login e senha) muda, praticamente, de 6 em 6 meses, mais ou menos, o que nesse nosso contexto significa raramente;
A página de descrição de um filme no site da Netflix ou do Prime Video raramente muda. Provavelmente nunca muda.
Então, voltando para nosso sisteminha, parece um mau negócio a gente consultar a base de dados a cada requisição para montar essas nossas páginas de descrição de um filme a partir de seu identificador. Se 100 usuários pedirem para ver a descrição de um mesmo filme, faremos 100 consultas diferentes à API que vão retornar a mesma coisa e gerar exatamente a mesma página. Todas essas 100 requisições geraram um processamento desnecessário no nosso servidor para montar as páginas, além de gerarem um tráfego de dados desnecessário entre nosso servidor e o servidor da API, além de gerar um tráfego desnecessário no servidor da API que vei receber e processar 100 fetchs iguais e da mesma máquina (nosso servidor).
E por que todos esses "desnecessários" em destaque? Bem, se a gente sabe que a página do filme vai mudar muito raramente, a gente pode montar todas as páginas de tudo quanto é filme no build do app, quando ele tiver sendo instalado, e deixar todas essas páginas em cache. Quando a solicitação for feita, a gente (leia-se, o NextJS) entrega logo a página prontinha, sem fetch, sem porra nenhuma, bem ligeirinho.
Claro que a gente vai gastar mais espaço de armazenamento e o build vai ser mais lento, mas a gente vai economizar processamento, que é negócio muito mais caro que espaço de armazenamento, além de economizar tráfego de rede (entre nosso servidor e a API), também mais caro que espaço de armazenamento.
- E como fazer isso, professor?
É bem mais simples do que parece.
A gente só precisa escrever uma função que "diga" para o NextJS quais são os identificadores de filmes e ele vai montar todas as páginas para todos os identificador e que essa função vier a prover.
Edite seu page.js e acrescente a seguinte função, sem mexer em nada da função Home...
export async function generateStaticParams() {
const allMovies = await fetch('https://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=lady').then((res) => res.json())
return allMovies.Search.map((movie) => ({
id: movie.imdbID,
}))
}
E pronto. A burocracia é bem simples aqui:
Sua função precisa chamar-se generateStaticParams, precisa estar no mesmo arquivo da sua rota dinâmica (nesse caso, o movies/[id]/page.js) e precisa retornar um vetor de objetos. Agora, preste bastante atenção: cada objeto JSON do retorno deve ter uma propriedade que "bata" com o nome da rota, que no nosso caso é id, por isso escrevemos a função de callback do map dessa forma:
(movie) => ({
id: movie.imdbID,
})
- Ah, professor, mas aquele fetch do generateStaticParams não vai trazer todos os filmes da API não, é só uma pesquisa pela palavra-chave "lady"?
Verdade. Mas se a gente tivesse uma base de dados nossa, a gente conseguiria trazer sim. No caso, o pessoal da API, dessa e de praticamente todas as outras, não oferece uma pesquisa da base de dados inteira, então estou usando uma pesquisa arbitrária para ilustrar a funcionalidade. Na verdade, a gente está fazendo cache de 10 páginas de filmes, porque o generateSataticParams tá retornando esse array aqui:
[ { id: 'tt4925292' }, { id: 'tt0048280' }, { id: 'tt8613070' }, { id: 'tt1007029' }, { id: 'tt0452637' }, { id: 'tt0058385' }, { id: 'tt0451094' }, { id: 'tt0030341' }, { id: 'tt0040525' }, { id: 'tt3722070' } ]
- Então deixe eu ver se eu entendi. Quer dizer que se eu acessar a página http://localhost:3000/movie/tt4925292, que é a página construída com aquele primeiro identificador do array retornado pelo generateStaticParams, a página já vai estar pronta e a função Home lá no page.js nem vai ser chamada?
É isso mesmo.
- Então eu nem vou ver aquela página "Loading", já que a página já tá pronta.
É isso mesmo.
- Professor, não é por nada não, mas acessando esse link (http://localhost:3000/movie/tt4925292) eu ainda vejo a mensagem Loading. Será que deu errada a geração estática.
Tá ficando sabido demais, mas não deu errado não.
Vamos à explicação.
Isso acontece porque você está rodando seu app em "modo dev", em modo de desenvolvimento. Nesse modo ele realmente não pré-renderiza as páginas.
A gente pode rodar o app em modo de produção, que é quando fazemos o build do projeto e instalamos a versão de produção servidor.
Abra o packge.json do seu projeto e modifique a entrada "scripts" para que fique da seguinte forma:
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"test": "echo \"Error: no test specified\" && exit 1"
},
Se você está acompanhando as receitas, bastará acrescentar as 2 linhas em desta. A primeira linha delas é para fazer o build do projeto, e nesse processo devem ser geradas as páginas estáticas, entre muitas outras coisas. A segunda linha é para iniciar um servidor local com o resultado do build instalado nele.
Agora, abra o Terminal no diretório do projeto e escreva pnpm build.
Você deve ver algo do tipo...
Aquela parte emdestaque indica que as 10 páginas foram geradas. Lembre, poderiam ser 10 mil ou 10 milhões, dependendo do retorno da função generateStaticParams.
Agora, digite pnpm start no terminal.
Isso vai subir um servidor local com a versão de produção produzida no build.
Agora sim, você não vai mais ver nenhum "Loading" para nenhuma das 10 páginas geradas estaticamente.
- Massa. Acabou, professor?
Na verdade, não. Tem mais um pequeno detalhe emocionante.
Você meio que indicou ao Next JS que a página dessa rota deve ser pré-renderizada, seja qual for o id dela. Então, quando você solicitar pela primeira vez o endereço http://localhost:3000/movie/tt2085059, que dá numa página que não foi gerada estaticamente, o NextJS vai fazer aquele esquema de mandar o "Loading", executar a função Home, gerar e mandar a página de volta. Na segunda vez em diante que você ou qualquer outro usuário requisitar esse link ele vai mandar a página pronta - ele vai fazendo cache de tudo de novo que não foi pré-renderizado ainda.
Se for o caso de você querer que apenas aquelas páginas pré-renderizadas estejam acessíveis, é só exportar a seguinte variável no page.js:
export const dynamicParams = false
Se você fizer isso, qualquer solicitação a alguma página que não foi pre-renderizada dará o famoso "erro 404".
- Maravilhoso. Agora acabou, né?
Na verdade, não.
- Ai, Jesus...
É que pode ser que alguma dessas informações que nunca mudam, bem, mudem. Afinal, isso é computação, bebê, e tudo muda por aqui.
Nesse caso, se você escrever a função que modifica o filme com identificador tt0030341 na base de dados (veja que essa API específica não nos oferece possibilidade de modificação), basta escrever...
revalidatePath('/movie/tt0030341')
... ao final da função que altera o filme. Isso vai descartar o cache para essa rota. No acesso seguinte, a página vai ser regerada.
É possível também estabelecer um intervalo regular para revalidação de toda uma rota dinâmica, sem necessidade de chamar o revalidatePath para nenhum valor específico. Basta exportar uma variável no /app/movies/[id]/page.js
export const revalidate = 60*60*24
Isso faria com que cada pre-renderização deixasse de valer a cada dia (o valor do revalidate é em segundos).
Esses esquemas de revalidação têm um nome técnico: Incremental Static Regeneration, ou ISR.
Diferentes tecnologias, claro, adotam burocracias distintas para implementar tanto o ISR, quanto o SSG e o Dynamic Routing.
Agora sim, acabou.
Pegue todas as páginas de pesquisa de filmes que você fez nas receitas anteriores e transforme os resultados da pesquisa em links para a rota dinâmica criada nessa receita. Lembre de usar a marca Link do NextJS
Mostre a imagem do poster do filme na página que criamos usando a marca Image do NextJS.