# Receita: Do Next.js para a Trilha > Framework web para Go no estilo Next.js: uma pasta em app/ é uma rota, o HTML é escrito em Go e nada fora da biblioteca padrão é importado. > /trilha/pt/llms/recipes/do-next.txt - [Do Next.js para a Trilha](/trilha/pt/receitas/do-next): Os padrões de React que você escreve sem pensar — useEffect + fetch, useState do modal, toast, useSearchParams — e a linha de Go que substitui cada um. · 3668 tokens est. - [Listagens](/trilha/pt/referencia/listagens): A ListParams lê o estado da listagem na URL; o ui.DataTable desenha. · 1967 tokens est. - [Fragmentos vivos](/trilha/pt/referencia/vivo): O ui.Poll atualiza um fragmento pelo relógio; ui.Live e ui.On atualizam quando o servidor avisa. · 1468 tokens est. - [ui](/trilha/pt/referencia/ui): Componentes do kit, variantes, assets e o contrato de tema. · 11965 tokens est. - [Shell](/trilha/pt/referencia/shell): ui.Shell é a moldura de um app interno — barra lateral, cabeçalho, menu do usuário — e ui.PageHeader é o título da tela dentro dela. · 1100 tokens est. 5 page(s) · 20168 tokens est. in all --- Esta página é para quem já monta telas em Next.js e agora olha para um arquivo Go se perguntando onde foi parar o estado. É uma tabela de tradução, não um argumento: o app que está mudando de casa é um painel de artigos — lista com filtro e ordenação, uma fila que se atualiza sozinha, um modal de confirmação, um PDF — e cada seção mostra o React que você escreveria e a Trilha que ocupa o lugar dele. A versão curta da página inteira: **o estado que morava no browser passa a morar na URL ou no servidor**, e o que sobra no browser é o pouco que o kit já traz pronto. | No app React | Aqui | |---|---| | `useEffect` + `fetch` + `useState(loading)` | uma função de página que lê os dados e devolve o HTML | | uma Server Action (`"use server"`) e `
` | um `POST` ao lado da página que grava e dá `c.Redirect` | | `useSearchParams` e um estado por filtro | [`trilha.ListParams`](/trilha/pt/referencia/listagens) lida pelo `c.Bind` | | `setInterval` + limpeza no `useEffect` | [`ui.Poll`](/trilha/pt/referencia/vivo) | | `toast()` de um provider | `c.Flash` | | `useState(open)` + portal + tratador de Escape | [`ui.Dialog`](/trilha/pt/referencia/ui) | | `createObjectURL` sobre um blob | `c.Inline` | | `if (!user) router.replace("/login")` | `Auth.Require()` no `middleware.go` do ramo | | estado da barra lateral no `localStorage` | [`ui.Shell`](/trilha/pt/referencia/shell) | | `dangerouslySetInnerHTML` | [`ui.Markdown`](/trilha/pt/referencia/ui) | | uma biblioteca de gráfico no cliente | `c.Island` | | `next.config.js` | `app/setup.go` e `Config` | Antes da primeira linha de Go, `trilha migrate next ../web` grava a árvore de pastas do `app/` e um `MIGRATION.md` dizendo, tela por tela, de qual arquivo veio, o que chamava e qual dos formatos abaixo ela provavelmente é. O esqueleto compila; o porte é o que sobra, e esta página é a referência dele. Veja [o comando](/trilha/pt/referencia/cli#trilha-migrate). ## A tela que busca O painel em React: um efeito, três estados e um render que precisa dizer algo sensato em cada um deles. ```js export default function Dashboard() { const [rows, setRows] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { fetch("/api/articles") .then(r => r.json()).then(setRows) .catch(setError).finally(() => setLoading(false)) }, []) if (loading) return if (error) return return } ``` A mesma tela aqui é uma função. Ela roda no servidor, então ler o banco é uma chamada de função e a página chega pronta: ```go // Dashboard is app/dashboard/page.go. There is no useEffect, no loading flag // and no error state: the function runs on the server, so it reads the // database the way any Go function does and returns the page already filled. // What React needed three renders for happens here before the first byte. func Dashboard(c *trilha.Ctx) (h.Node, error) { var q DashQuery if err := c.Bind(&q); err != nil { return nil, err } rows, total, err := dashRows(c.Context(), q) if err != nil { return nil, err } return ui.DataTable(c, DashColumns, rows, ui.ListState{ Params: q.ListParams, Total: total, ID: "articles", Search: "Search articles", Empty: h.Text("No article matches this filter."), RowHref: func(i int) string { return "/dashboard/" + rows[i].Slug }, }), nil } ``` Não há estado de carregando porque não existe o instante em que a página existe sem os dados, e não há estado de erro porque o erro é um `error` devolvido — o framework transforma nele o status e a página que a pessoa vê. ## Os filtros que eram estado Cada filtro do app React era um `useState` mais um `useSearchParams` para guardá-lo no endereço, mais um efeito para buscar de novo quando qualquer um dos dois mudasse. Aqui o endereço *é* o estado: ```go // DashQuery is the whole state the dashboard used to keep in React: the page, // the ordering, the search box and the type filter. Embedding ListParams is // what makes them live in the address instead of in memory — the visitor can // bookmark the screen, reload it, or send it to somebody else. type DashQuery struct { trilha.ListParams Status string `form:"status"` } ``` O `c.Bind` preenche as duas metades a partir da query, aplica os limites de uma listagem (página no mínimo 1, tamanho de página com teto) e devolve a struct. As colunas dizem por quais delas dá para ordenar: ```go // DashColumns replaces the written by hand and the onClick that // re-sorted the array in the browser. Sort: true turns the header into a real // link — the ordering is a GET, so it works with the script blocked and it is // still there after a reload. var DashColumns = ui.Columns[Article]{ {Key: "title", Label: "Title", Sort: true, Cell: func(a Article) h.Node { return h.Text(a.Title) }}, {Key: "published", Label: "Published", Sort: true, Cell: func(a Article) h.Node { return h.Text(a.Published.Format("2006-01-02")) }}, } ``` E a consulta é onde a URL deixa de ser confiável: ```go // dashRows is the query behind the screen. The ordering comes from the URL, // so it is checked before it reaches SQL: Restrict drops a column nobody // declared, and what is left is one of two names this function knows. func dashRows(ctx context.Context, q DashQuery) ([]Article, int, error) { q.Restrict("title", "published") order := `published_at DESC` if q.Sort == "title" { order = `title` if !q.Asc() { order = `title DESC` } } rows, err := DB.QueryContext(ctx, `SELECT id, slug, title, published_at FROM articles WHERE ($1 = '' OR title ILIKE '%' || $1 || '%') AND ($2 = '' OR status = $2) ORDER BY `+order+` LIMIT $3 OFFSET $4`, q.Q, q.Status, q.Limit(), q.Offset()) if err != nil { return nil, 0, err } defer rows.Close() var out []Article for rows.Next() { var a Article if err := rows.Scan(&a.ID, &a.Slug, &a.Title, &a.Published); err != nil { return nil, 0, err } out = append(out, a) } if err := rows.Err(); err != nil { return nil, 0, err } var total int err = DB.QueryRowContext(ctx, `SELECT count(*) FROM articles WHERE ($1 = '' OR status = $1)`, q.Status).Scan(&total) return out, total, err } ``` ## O intervalo que vazava ```js useEffect(() => { const id = setInterval(() => refetch(), 6000) return () => clearInterval(id) }, []) ``` O fragmento pede a si mesmo, e quem decide quando parar é o servidor: ```go // Queue is the block that used to be a useEffect with a setInterval and a // cleanup nobody remembered to write. ui.Poll asks this same route for the // fragment every six seconds; when the work is done the handler stops the // clock from the server side and the browser stops asking. func Queue(c *trilha.Ctx) (h.Node, error) { left, err := pendingArticles(c.Context()) if err != nil { return nil, err } if left == 0 { c.PollStop() return h.Div(h.ID("queue"), h.Text("Everything processed.")), nil } return h.Div(h.ID("queue"), ui.Poll("6s", "#queue"), h.Text(strconv.Itoa(left)+" articles in the queue"), ), nil } ``` `c.PollStop()` escreve um cabeçalho que o kit lê; `c.PollEvery(d)` muda o intervalo do mesmo lugar. O browser ainda pausa enquanto a aba está escondida — comportamento que você teria de escrever e de lembrar de escrever de novo na tela seguinte. ## O toast `toast.success("Artigo publicado")` precisa de um provider na raiz, de um portal e de um estado que sobreviva à navegação. O flash é escrito antes do redirect e mostrado pela página que vem depois: ```go // Publish is the POST behind the button. The toast() of the React app is a // flash here: the message is written before the redirect and shown by the // page that comes next, so a refresh does not repeat the action and the // message survives the navigation without any state to carry. func Publish(c *trilha.Ctx) error { if _, err := DB.ExecContext(c.Context(), `UPDATE articles SET status = 'published' WHERE slug = $1`, c.Param("slug")); err != nil { return err } c.Flash(ui.FlashSuccess, "Article published") return c.Redirect("/dashboard") } ``` ## O modal O modal em React é um `useState(false)`, um portal, um efeito para o Escape e um foco preso por biblioteca. O `` faz os quatro: ```go // ConfirmDelete is the modal that used to be a useState(false), a portal and // an effect closing it on Escape. The dialog is in the HTML from the start, // closed; the browser opens it, traps the focus and closes it on Escape by // itself, because already does all of that. func ConfirmDelete(slug string) h.Node { return h.Div( ui.DialogTrigger("delete", h.Text("Delete")), ui.Dialog("delete", "Delete this article?", h.P(h.Text("This cannot be undone.")), h.Form(h.Method("post"), h.Action("/dashboard/"+slug+"/delete"), ui.Button(ui.Destructive(), h.Text("Delete")), ), ), ) } ``` ## O blob ```js const res = await fetch(`/api/preview/${slug}`) const url = URL.createObjectURL(await res.blob()) setSrc(url) // e revogue, se o componente viver até lá ``` Um endereço é mais simples que um blob, e sobrevive a um recarregamento: ```go // Preview answers the PDF the