Objeto: criação de uma página que é construída com base em interação com com um sistema externo, onde esse sistema externo é implementado pelo próprio desenvolvedor, na forma de server functions, que são uma versão mais prática das API's. A interação com as server functions é realizada a partir de um "client component".
Principais termos técnicos abordados: server actions, NextJS, react hook, useState, fetch, async, await, JavaScript, HTML
Requisitos para as instruções funcionarem: ter concluído a Receita4 e estar com o projeto resultante aberto
Requisitos para compreensão das instruções: noções bem básicas de programação, HTML e JS/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
A gente aprendeu a fazer nossa própria API e a interagir com ela através da função fetch, executada num "client component". É razoavelmente simples o processo: do client component chamamos a função fetch, montando os parâmetros na forma de URL, essa chamada vai bater numa função que escrevemos e roda no nosso servidor, essa função recebe um objeto que representa uma requisição HTTP, tira os parâmetros que a interessam desse objeto, executa o que tem que executar e retorna uma resposta HTTP para o fetch, daí convertemos essa resposta para JSON e usamos o JSON para montar alguma interface gráfica.
A implementação é razoavelmente simples, mas programador é bicho preguiçoso e a gente acha muito burocrático tudo isso que escrevi em negrito. Os caras inventaram as "server actions" para acabar com todo esse moído.
O objetivo é escrever uma API sem a burocracia que a API impõe, sem precisar nem saber que o protocolo HTTP existe, pra início de conversa. O "http request" vai ser o parâmetro que você passar e o "http response" vai ser o objeto que você retornar, simples assim.
Basicamente, a gente vai escrever uma ruma de função numa rota sem page.js e sem route.js. Essa ruma de função vai poder receber parâmetros facilmente transmissíveis via web - Strings, inteiros, mapas, dados de formulário objetos JSON etc. Essa ruma de função vai poder retornar, basicamente, o mesmo tipo de coisa que pode receber como parâmetro - Strings, inteiros, mapas, dados de formulário objetos JSON etc. Essa ruma de função vai rodar no servidor e, portanto, poder acessar a base de dados sem maiores problemas. E - aqui vem a emoção maior - essa ruma de função vai poder ser chamada de "client components" e "server components" como se chama qualquer função: importe, escreva o nome da função em algum canto, passe os parâmetros e se divirta com o retorno! Toda a burocracia por debaixo dos panos será realizada pelo Next JS!!!
A gente simplesmente tem que aprender isso!
E vamos aprender fazendo a mesma funcionalidade
Vamos lá!
Crie uma nova rota /app/actions/movieActions.js.
Nesse arquivo podemos escrever várias server actions, e podemos ter outras rotas com outras server actions, vai depender do tamanho do seu app e de como você vai querer organizar suas funções internamente. Provavelmente você pode até por server actions em rotas que têm um page.js ou route.js, mas é bom não misturas as coisas.
Edite o movieActions.js
'use server'
export async function searchMovies(formData){
const titleSearchKey = formData.get("titleSearchKey")
if (!titleSearchKey || titleSearchKey=='') return {Search: []}
try{
const httpRes = await fetch(`http://www.omdbapi.com/?apikey=ME_SUBSTITUA&s=${titleSearchKey}`)
const jsonRes = await httpRes.json()
return jsonRes
}catch(err){
return {error: `Erro na requisição ${err}`}
}
}
Bem, fora a diretriz 'use server' lá no topo do arquivo, essa é uma função como qualquer outra função JS. Esse formData recebido como parâmetro representará um formulário React com todos os dados nele contidos - o mesmo objeto com o qual a gente já trabalhou nas receitas anteriores.
Não custa nada a gente lembrar que essa implementação é uma alternativa à implementação da "API endpoint" da receita anterior. E não custa nada a gente dar uma olhada como ficou nossa função naquela abordagem, para comparar com essa acima...
export async function GET(request){
const searchParams = request.nextUrl.searchParams
const titleSearchKey = searchParams.get("titleSearchKey")
const httpRes = await fetch(`http://www.omdbapi.com/?apikey=f1cbc41e&s=${titleSearchKey}`)
const jsonRes = await httpRes.json()
return Response.json({...jsonRes})
}
O que a gente precisa, agora, é chamar nossa server action.
E vamos fazer isso de um "client component": um componente que roda no cliente vai chamar uma função que roda no servidor e receber seu retorno como se tivesse chamando a função localmente.
Crie uma nova rota app/clientServerMovies/page.js. Deixe o page.js ao estilo das receitas anteriores em que se chamava uma API a partir de um client component:
"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=ME_SUBSTITUA&s=${titleSearchKey}`)
const jsonRes = await httpRes.json()
setResultMovies(jsonRes.Search || [])
}
return (
<div>
<MovieForm handleAction={handleAction}/>
<MovieTable movies={ resultMovies }/>
</div>
)
}
export function MovieForm({actionHandler}){
return (
<Form action={actionHandler}>
<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>
)
}
Veja, a gente quer se livrar de uma ruma de coisa que tem naquela parte em negrito, e segure a respiração, que é muita doidice para apenas 2 linhas de código.
A gente quer se livrar do fetch.
A gente quer se livrar do endereço do servidor na URL - http://www.omdbapi.com (ou o caminho da rota da sua API).
A gente quer se livrar de montar os parâmetros na URL com tudo que é símbolo do teclado (?, &, $, =, { ) e ainda com injeção de valores dentro de um string - ?apikey=f1cbc41e&s=${titleSearchKey}
E como a gente é banqueiro mermo, a gente quer se livrar de converter a resposta do servidor ao fetch em objeto JSON. Queremos o JSON direto, limpo e seco.
Das 2 linhas destacadas, para não pagar de intolerante, a gente aceita ficar com o await.
Porque aí também já seria o poste mijando no cachorro.
Modifique os imports e o componente Home de seu page.js (os componentes MovieForm e MovieTable permanecem iguais, não os apague)
"use client"
import {searchMovies} from "../actions/movieActions"
import Form from "next/form"
import {useState} from "react"
export default function Home(){
const [data, setData] = useState({})
async function handleAction(formData){
const res = await searchMovies(formData)
setData(res)
}
return (
<div>
<MovieForm actionHandler={handleAction}/>
{data.Search && <MovieTable movies={data.Search}/>}
</div>
)
}
Importar, chamar, passar parâmetro e usar o resultado. Mais mel de rapadura com queijo de coalho do que isso, impossível.
Você pode tentar inspecionar o código-fonte da página e você não vai achar a implementação da sua server action searchMovies de jeito nenhum. Ela está guardadinha lá no servidor.
Salve seu arquivo de rota e teste-o, deve dar tudo certo.
As server actions estão menos expostas que as API's. Embora o código da função não esteja disponível para um usuário malicioso (o da API também não está), a chamada da server action está, sim, exposta (como a chamada à API está). É mais difícil do cara achar o identificador de uma server function e a maneira chamá-la, mas está bem longe de ser impossível.
Então, tal qual acontece com os "API endpoints" que a gente implementa, as server actions também necessitam de algumas considerações de segurança, como restrição de acesso a usuários logados etc.
Documentação oficial das server actions
Que tal acrescentar um campo a mais nesse formulário e adaptar sua server action para refinar a pesquisa?