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, a partir de um "client component", um componente que é renderizado no navegador. Utilizaremos o hook useState 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 adaptação do sistema que implementamos na receita anterior. A gente criou um componente que rodava no servidor. Esse componente recebia parâmetros de requisição e esses parâmetros eram enviados ao servidor através de um formulário. O componente recebia esses parâmetros, consultava uma API externa e montava uma página com base nos dados que retornavam da consulta à API externa.
A gente vai fazer quase a mesma coisa.
Quase.
Continuamos tendo um formulário de pesquisa, ele será um "client component", um componente que deve ser renderizado no navegador em vez de no servidor - em síntese, o navegador vai invocar sua função Home e renderizar o resultado dela. Ter uma página como um "client component" nos permite acrescentar a ela algumas coisas emocionantes e interativas, como manipulação de eventos, validação de dados no navegador e gerência de estados através das tradicionais funções React conhecidas como "react hooks".
Mas vamos começar devagar e eu vou explicando cada nova aspecto conforme ele vá aparecendo.
Queremos uma página com um formulário de pesquisa e uma tabela de resultados e queremos que essa página seja renderizada no navegador - no cliente para usar a linguagem do Next/React.
Crie uma nova rota app/clientMovies1/page.js com o seguinte conteúdo.
"use client"
import Form from "next/form"
export default function Home(){
return (
<div>
<MovieForm/>
<MovieTable movies={ [] }/>
</div>
)
}
export function MovieForm(){
return (
<Form>
<label htmlFor="idTitleSearchKey">Título</label>
<input id="idTitleSearchKey" name="titleSearchKey"/>
<button type="submit">Pesquisar</button>
</Form>
)
}
export function MovieTable({movies}){
return (
<div>
<div>
{movies.map( (m) => <div key={m.imdbID}>{m.Title} --- {m.Year}</div> )}
</div>
</div>
)
}
A cara desse código deve ficar assim...
Fora o "use client" que a gente escreveu lá no início, não tem muita novidade. Usado ali no início do arquivo como a gente usou, essa diretiva indica que todos os componentes e funções do arquivo estarão visíveis e serão renderizados no navegador, a menos que haja alguma diretiva em contrário dentro de algum componente específico. Por default, todos os componentes React/Next são server components, como todos os que a gente escreveu até agora - eles são executados/renderizados, por default, no servidor. Se você não escrever nenhuma diretiva, o componente será um server component.
Fora esse "use client", temos uma página Home associada à rota, e essa página é composta de dois componentes definidos ali mesmo no arquivo page.js, o MovieForm e o MovieTable. A gente só não enxerga o MovieTable na interface gráfica porque estamos passando uma lista vazia como parâmetro para o componente (na instrução movies = { [] } ) .
Agora vamos acrescentar alguma interatividade, vamos reagir ao evento de pressionar o botão Pesquisar. A questão é onde escreveremos a função que vai ser invocada quando da chamada submissão do formulário.
Vamos começar escrevendo a manipulação do evento no lugar mais intuitivo para depois escrever no lugar mais adequado.
Modifique o arquivo page.js:
"use client"
import Form from "next/form"
export default function Home(){
return (
<div>
<MovieForm/>
<MovieTable movies={ [] }/>
</div>
)
}
export function MovieForm(){
function handleAction(formData){
console.log(formData.get("titleSearchKey"))
}
return (
<Form action={handleAction}>
<label htmlFor="idTitleSearchKey">Título</label>
<input id="idTitleSearchKey" name="titleSearchKey"/>
<button type="submit">Pesquisar</button>
</Form>
)
}
export function MovieTable({movies}){
return (
<div>
<div>
{movies.map( (m) => <div key={m.imdbID}>{m.Title} --- {m.Year}</div> )}
</div>
</div>
)
}
- Vige, professor, uma função definida dentro de uma outra função...?
Pois é. Poderia estar definida fora também, mas é perfeitamente possível e, no caso de manipulação de eventos, é comum que se defina a função de manipulação dentro da função mais ampla que define o componente em si. Assim a função de manipulação (handleAction) enxerga tudo relativo à função que define o componente (MovieForm). Se houvesse variáveis locais ou parâmetros, a função de manipulação poderia acessar tudo, por exemplo.
No mais, usamos a propriedade action do Form para estabelecer a ligação entre a submissão do formulário e a execução da função handleAction. A função handleAction, por sua vez, apenas recebe um dataForm, que é um objeto que contém todas as informações do formulário submetido.
Salve e acesse a rota http://localhost:3000/clientMovies1 para ver o resultado.
Se você digitar qualquer coisa no formulário, essa qualquer coisa vai ser cuspida no console após o preenchimento e o clique em Pesquisar...
... deve aparecer o nome digitado no console de inspeção de código.
Então, precisamos lembrar de dois podes e um deve para bem compreendermos as coisas:
a função de manipulação para submissão pode ser escrita dentro da própria função do componente
ela deve receber um dataForm como parâmetro (nada de evento, como no "JS papai e mamãe") e
ela pode ser referenciada na propriedade action do Form (há outras maneiras, explorei apenas essa)
Sendo que a gente não quer dar um console.log no que o usuário digitar no formulário, não é mesmo? A gente quer ir buscar dados numa API (com a nossa querida função fetch) e fazer com que esses dados sejam transformados em interface gráfica através do componente <MovieTable>.
- E quem tem que fazer o fetch, professor?
Bem, existe uma informação essencial para fazermos esse fecth, a gente precisa da chave de pesquisa.
- Ah, já sei, quem tem que fazer o fetch é a função handleAction, porque ela tem acesso à chave de pesquisa através do objeto dataForm, certo?
Tá certo e tá errado.
- Djabo é, homi...
Teoricamente, seria lá mesmo. Mas vamos escrever esse fetch lá e analisar os problemas.
Modifique seu componente MovieForm...
export function MovieForm(){
function async handleAction(formData){
const titleSearchKey = formData.get("titleSearchKey")
const httpRes = await fetch(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=${titleSearchKey}`)
const jsonRes = await httpRes.json()
console.log(jsonRes)
}
return (
<Form action={handleAction}>
<label htmlFor="idTitleSearchKey">Título</label>
<input id="idTitleSearchKey" name="titleSearchKey"/>
<button type="submit">Pesquisar</button>
</Form>
)
}
Do jeito que está escrita a função handleAction ela vai fazer o fetch direitinho, diretamente a partir do navegador, sem passar por nenhum componente hospedado em nosso servidor. Esse resultado vai chegar e vai bater na variável jsonRes, na forma de objeto JSON. A grande questão é: o que djabo a gente pode fazer com isso? Ou, em outras palavras, como fazer o componente MovieTable receber esse jsonRes para poder fazer sua arte e montar uma interface gráfica para as informações recebidas?
Em, não tem jeito de se fazer isso. Nosso formulário não conhece nossa tabela, e nem é para conhecer. Nossa tabela também não conhece ninguém senão a si própria, e nem é para conhecer. No entanto, a gente precisa que uma ação oriunda do formulário desencadeie uma ação e termine gerando efeitos colaterais na tabela. O ideal seria que esse processo fosse controlado por alguém que conhecesse o formulário e a tabela, e agente tem esse alguém: é o componente/página Home!
Então vamos fazer duas coisas, primeiro vamos retirar do formulário essa "inteligência" de fazer a requisição e transferi-la para o componente Home.
Modifique o arquivo page.js.
"use client"
import Form from "next/form"
export default function Home(){
function async handleAction(formData){
const titleSearchKey = formData.get("titleSearchKey")
const httpRes = await fetch(`http://www.omdbapi.com/?apikey=f1cbc41e&s=${titleSearchKey}`)
const jsonRes = await httpRes.json()
console.log(jsonRes)
}
return (
<div>
<MovieForm/>
<MovieTable movies={ [] }/>
</div>
)
}
export function MovieForm(){
return (
<Form action={handleAction}>
<label htmlFor="idTitleSearchKey">Título</label>
<input id="idTitleSearchKey" name="titleSearchKey"/>
<button type="submit">Pesquisar</button>
</Form>
)
}
O problema agora é que o MovieForm não enxerga mais a função handleAction. Essa parte em vermelho vai causar erro.
Vamos contornar isso. O nosso formulário vai "terceirizar" a manipulação do evento que acontece dentro dele (a submissão do formulário). Isso é feito de forma bem simples. Primeiro, o componente deve poder receber uma função como parâmetro, como qualquer função de callback.
Modifique o componente do formulário:
export function MovieForm({handleAction}){
return (
<Form action={handleAction}>
<label htmlFor="idTitleSearchKey">Título</label>
<input id="idTitleSearchKey" name="titleSearchKey"/>
<button type="submit">Pesquisar</button>
</Form>
)
}
Se ligue que o handleAction dadeclaração da função MovieForm tem que estar entre chaves. Isso já foi explicado anteriormente, em outras receitas.
Bem, agora o formulário é capaz de receber uma função de quem o usa e invocar essa função quando o formulário for submetido.
Quem usa o formulário, no caso, é o Home, então falta só reescrever o Home para passar a função handleAction para o MovieForm...
export default function Home(){
async function handleAction(formData){
const titleSearchKey = formData.get("titleSearchKey")
const httpRes = await fetch(`http://www.omdbapi.com/?apikey=f1cbc41e&s=${titleSearchKey}`)
const jsonRes = await httpRes.json()
console.log(jsonRes)
}
return (
<div>
<MovieForm handleAction={handleAction}/>
<MovieTable movies={ [] }/>
</div>
)
}
E agora, como passamos a resposta da requisição para o componente MovieTable?
Vamos na intuição, inicialmente.
Modifique o arquivo page.js para que assim fique:
"use client"
import Form from "next/form"
export default function Home(){
let resultMovies = []
async function handleAction(formData){
const titleSearchKey = formData.get("titleSearchKey")
const httpRes = await fetch(`http://www.omdbapi.com/?apikey=f1cbc41e&s=${titleSearchKey}`)
const jsonRes = await httpRes.json()
resultMovies = jsonRes.Search || []
}
return (
<div>
<MovieForm handleAction={handleAction}/>
<MovieTable movies={ resultMovies }/>
</div>
)
}
export function MovieForm({handleAction}){
return (
<form action={handleAction}>
<label htmlFor="idTitleSearchKey">Título</label>
<input id="idTitleSearchKey" name="titleSearchKey"/>
<button type="submit">Pesquisar</button>
</form>
)
}
export function MovieTable({movies}){
return (
<div>
<div>
{movies.map( (m) => <div key={m.imdbID}>{m.Title} --- {m.Year}</div> )}
</div>
</div>
)
}
As mudanças estão destacadas em negrito.
Declaramos uma variável local...
let resultMovies = []
... Essa variável é visível de dentro da função que faz a pesquisa, então alteramos essa variável, atribuindo a ela o resultado da pesquisa em vez de fazermos console.log...
resultMovies = jsonRes.Search || []
... E por fim, ao montarmos o componente <MovieTable> passamos os filmes resultantes para ele...
<MovieTable movies={ resultMovies }/>
Tudo lindo e maravilhoso, não é mesmo?
- É professor, mas quando eu salvei e testei, além dele apagar o formulário quando eu clico em Pesquisar, não exibe nenhum resultado.
Precisamente.
Vamos entender o que está acontecendo. Você acessa a página e seu navegador a renderiza. Nessa primeira renderização, ninguém escreveu nada no formulário e tampoco clicou em nenhum botão, então seu componente MovieTable vou chamado com valor vazio para a propriedade movies. Ele corretamente não exibiu nada, porque não havia nada a exibir.
Nesse ponto, não há mais o que se fazer. Não existe ainda nenhuma variárel em que a gente possa mexer que vá ter como efeito colateral a renderização da tabela. Todo nosso esforço para a pesquisa, por enquanto, é inútil.
- E não dá pra usar o objeto document pra renderizar os dados não, professor? Usando getElementById e innerHTML como a gente fazia?
De jeito nenhum. Tecnicamente, você pode fazer isso, mas será um erro grotesco.
- Mas por quê?
Porque não se trata somente de fazer a coisa funcionar, tem que fazer a coisa funcionar direito, do jeito certo. A gente está usando o React para renderizar nossos componentes, então qualquer ajuste ou redesenho de partes da interface gráfica tem que ser via React. Acabou document, innerHTML e afins. O React interage com isso agora e a gente interage com o React.
- E como fazemos, então?
Precisamos usar uma estratégia de gerência de estados, de forma que a gente conecte dados com um determinado componente e, quando esses dados forem modificados, o React resesenhe todos os componentes conectados a ele.
A gerência de estados clássica e mais simples de entender, no mundo React, é o chamado useState, que faz parte de uma família de funções utilitárias conhecidas como hooks. No mundo React, todo hook tem que iniciar como nome use e você deve evitar usar esse nome em suas funções "normais". Tem hook pra tudo no mundo no React, e esse a gente usa quando quer fazer gerência de estados. Vamos ver como funciona.
Modifique o import de seu arquivo page.js e o componente Home...
"use client"
import Form from "next/form"
import {useState} from "react"
export default function Home(){
const [resultMovies, setResultMovies] = useState([])
async function handleAction(formData){
const titleSearchKey = formData.get("titleSearchKey")
const httpRes = await fetch(`http://www.omdbapi.com/?apikey=f1cbc41e&s=${titleSearchKey}`)
const jsonRes = await httpRes.json()
setResultMovies(jsonRes.Search || [])
}
return (
<div>
<MovieForm handleAction={handleAction}/>
<MovieTable movies={ resultMovies }/>
</div>
)
}
Vamos ver a mágica acontecendo pra depois discutirmos as mudanças no código...
Salvando, acessando http://localhost:3000/clientMovies1 e digitando "girls" (digite o que quiser)...
...Clicando em Pesquisar...
Agora vamos entender o código. Não vou explicar o import, e espero que você não precise.
Escrevemos a chamada ao hook...
const [resultMovies, setResultMovies] = useState([])
... logo na primeira linha da função Home. Se ligue logo nisso. Hook tem que ser escrito no início das funções. Pode ter uma ruma de hook, mas não pode ter outras chamadas antes deles e nem pode ter chamada a hook dentro de if.
- Mas o que essa chamada quer dizer?
Quer dizer que o React vai gerenciar o estado de seu componente Home, onde o useState foi escrito. O useState retorna dois valores (um vetor de dois valores, mais precisamente). Um (resultMovies) é o estado corrente do componente (Home) onde se escreve o useState, o outro é uma função (setResultMovies) que deve ser chamada quando quisermos modificar o estado do componente. O parâmetro do useState é o valor do estado inicial, quando o componente for renderizado pela primeira vez. Como o estado inicial é uma lista/array, podemos passar qualquer lista/array como parâmetro para a função setResultMovies.
Nesse caso, o React nos garante que sempre que a função setResultMovies for invocada passando-se um parâmetro diferente do estado corrente do componente Home, o componente Home vai ser redesenhado.
E estamos justamente chamando essa função quando os resultados da pesquisa chegam, na linha...
setResultMovies(jsonRes.Search || [])
Essa chamada deve acarretar uma nova renderização do componente, com o useState retornando, na re-renderização, o novo valor do estado.
De resto, apenas passamos o valor corrente do estado para o componente MovieTable...
<MovieTable movies={ resultMovies }/>
Eu compreendo que, para quem está acostumado com a programação "the book is on the table", com algoritmos pra descobrir se um número é primo ou listar a série de Fibonacci, essa chamada ao useState pode parecer estranha e o funcionamente geral difícil de entender.
Porque o useState trata-se de uma função que é invocada sempre com o mesmo parâmetro (o array []) e, ainda assim, sempre retorna valores diferentes.
Vou explicar, resumidamente, como as coisas acontecem nesse caso específico que a gente tratou, para tentar facilitar as coisas. Vou simplificar a narrativa, se não vira um romance.
A página é acessada pelo navegador, que recebe uma ruma de componentes React e os renderiza.
Nessa primeira renderização, o useState retorna um array com uma lista vazia e uma função de mudança de estado. Guardamos o array vazio na variável resultMovies e a função na variável setResultMovies.
A gente digitou a palavra "girls" e empurrou um clique em Pesquisar. O navegador chamou a função handleAction.
a função handleAction fez uma pesquisa via fetch, usando os dados do formulário, e chamou a função de mudança de estados passando o resultado da pesquisa como parâmetro pra ela.
a função modificadora de estado, bem, modificou o estado do componente. Com isso, o React entende que tem que redesenhar aquele componente Home (o que vai redesenhar os outros que estão abaixo dele), a quem o estado está associado.
O React invoca a função Home novamente, para computar seu resultado e redesenhar a parte devida da interface gráfica. Note que você não chamou nunca essa função, você apenas modificou os dados que representam o estado dela.
Quando a função Home é executada novamente, aquela função useState não vai retornar um valor vazio para o primeiro elemento do vetor, vai retornar o valor que você passou como parâmetro quando chamou o setMovieResults, ou seja, o useState vai retornar o resultado de sua pesquisa e a mesma função modificadora de estado que tinha retornado da primeira vez. Esse é o "pulo do gato". A mesma função, chamada do mesmo canto, com o mesmo parâmetro, retornando coisa diferente a cada chamada.
Tudo que estiver subordinado ao Home é redesenhado, porque a função é executada novamente, do início ao fim. Quando chega a hora de redesenhar o MovieForm, o valor digitado é perdido. A gente não guardou ele em canto nenhum, ele não faz parte do estado, a gente não tá passando ele como parâmetro pro MovieForm, então o componente MovieForm vai ser "zerado". Já o componente MovieTable vi receber como parâmetro o estado que o useState retornou e que a gente guardou na variável resultMovies. Bingo! O MovieTable transforma o array de JSON em interface gráfica.
Há cenários em que pode ser interessante a gente fazer um fetch do navegador, em especial quando acessamos uma API externa aos nossos sistemas e sem chaves de acesso. Nesse caso, como essa API envolve uma chave de acesso, a melhor forma de lidar com ela é realmente através do servidor. Mas precisamos aprender as duas abordagens!
Agora, vamos brincar de fazer exercícios.
Resolva o problema de zerar o estado do formulário. Você pode fazer uma segunda chamada a useState para manter a chave de pesquisa no estado do componente.
Desabilite o botão de pesquisar enquanto a requisição estiver sendo processada e reabilite quando os resultados chegarem.
Faça com que o formulário seja submetido também quando teclamos <enter>
Bonitifique sua página com estilos CSS na tabela e no formulário. Tente usar a biblioteca tailwind, mas pode usar outras, se desejar.
Desafio das galáxias. Mantenha a estrutura da página como está, um componente Home que chama os componentes MovieForm e MovieTable. Quando a gente põe a chave de pesquisa no estado do componente Home, a gente tem condições de preservar o valor digitado no formulário. Mas a verdade é que o formulário vai continuar sendo redesenhado e, nesse nosso cenário, não existe nenhuma razão para esse formulário ser renderizado mais de uma vez. Implemente essa funcionalidade, a de fazer com que apenas a tabela (e não a tabela e o formulário) seja redesenhada a cada nova pesquisa. Lembre, a estrutura geral do arquivo tem que ser preservada. O Home tem o MovieForm e o MovieTable dentro de si. Pode usar outros hooks e o que bem entender, apenas mantenha essa estrutura para o desafio ficar mais emocionante. Você pode acrescentar console.log nos componentes para saber quais estão ou não estão sendo redesenhados, mas mantenha uma coisa em mente: o componente em que você escrever o useState vai ser redesenhado, junto com todo componente que estiver abaixo dele.