Async React com NextJS 13

oieduardorabelo

Eduardo Rabelo

Posted on January 26, 2023

Async React com NextJS 13

Quer ver algo legal? O React está recebendo suporte nativo ao async e você já pode experimentar 😍

Em breve, isso funcionará em qualquer lugar:

const  ShowData  =  async  ()  =>  {
  const res =  await  fetch("/data-source")
  const json = res.json()
  return  <p>{json.text}</p>
}
Enter fullscreen mode Exit fullscreen mode

Observe o componente React async, o await em seu corpo, a completa falta de estados, efeitos, ganchos ou bibliotecas de carregamento. Apenas funciona. Você pode usar esse componente em qualquer lugar da sua árvore - mesmo em um componente que ele próprio não é async!

Isso faz parte do React's RFC: Suporte de primeira classe para promessas e async/await. Um próximo passo do React Suspense, sobre o qual escrevi em React 18 and the future of async data.

O objetivo é facilitar o uso do React Suspense. Funcionou!

Você pode usar o Async React no NextJS 13

No momento, a melhor maneira de experimentar o suporte do Async React é com o lado beta do NextJS 13 – o /app diretório. Eu usei para construir ScholarStream.ai e parece estranho, mas funciona muito bem.

Você pode veer meu código completo no GitHub.

O /app diretório faz uso dos Server Components – componentes React que são renderizados no servidor e enviam HTML. Sem nenhum JavaScript do lado do cliente! Cada re-render volta ao servidor.

Agora eu ouço você pensando "Mas Swiz, isso é lento como uma merda!? Não inventamos aplicativos de página única porque as viagens de ida e volta ao servidor demoram muito??"

Sim nós fizemos a volta completa. Mas os servidores percorreram um longo caminho desde então.

Meu entendimento é que a Vercel, empresa por trás do NextJS, faz uso extensivo de funções Serverless e Edge para executar o menor servidor possível o mais próximo possível do usuário para renderizar cada componente. Como o Remix, eles precisam de um compilador personalizado incorporado ao NextJS para fazer esse trabalho sem problemas.

Renderizadores híbridos com NextJS, imagem de documentos

Quando um componente do servidor precisa renderizar, uma nova função é executada apenas para essa renderização. Com a configuração correta (e pagando por isso), essa função é executada em uma plataforma semelhante a uma CDN, que visa baixa latência. A função retorna apenas o HTML desse componente e NextJS substitui a seção correta da sua interface do usuário no navegador.

Não sei o quão possível/fácil é fazer isso sem a Vercel. Em teoria, o NextJS é uma estrutura autônoma que funciona muito bem sem a Vercel.

Sim, isso significa que, mesmo com os componentes do servidor, ainda resta muito JavaScript do lado do cliente. Mas há menos :)

Como é o Async React com NextJS beta?

Novamente, código completo no GitHub 👉 https://github.com/Swizec/ScholarStream.ai. Veja em ação 👉 ScholarStream.ai

A base é usar a nova e opinativa estrutura do NextJS 13 na pasta /app:

  • page.tsx para a página
  • layout.tsx para o layout estático
  • loading.tsx para o estado de carregamento

page.tsx

Componentes de página são sempre componentes do servidor. O NextJS renderiza no servidor, armazena em cache o resultado e retorna.

// app/[route]/page.tsx
export default async function Home() {
  return (
    <main className={styles.main}>
      <Pitch />

      <h2>Read about:</h2>
      <TopicsList />

      {/* @ts-expect-error Server Component */}
      <Feed topic="cs.AI" count={5} isLast />
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

Você precisa dizer ao TypeScript para esperar um erro ao usar components async (de servidor) dentro de uma árvore JSX. Funciona, mas os tipos ainda não sabem disso. A equipe do NextJS está trabalhando para atualizar isso atualmente.

<Feed> nesse caso, é o componente que executa o carregamento de dados assíncronos. O NextJS lida perfeitamente com esse componente.

layout.tsx

Componentes de layout diz ao NextJS o que sempre renderiza em torno de sua página. Quando você aninha subdiretórios para fazer rotas complexas, seus layouts também serão aninhados.

// app/[route]/layout.tsx
export default function RootLayout({
  // Layouts devem aceitar uma propriedade "children"
  // Ele será preenchido por layouts aninhados ou páginas
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <Script
        src="https://plausible.io/js/script.js"
        data-domain="scholarstream.ai"
      />
      <body>
        <nav className={styles.topNav}>
          <Link href="/about">About</Link>
        </nav>
        {children}
        <div className={styles.footer}>
          built with reckless abandon by <a href="https://swizec.com">Swizec</a>
          <br />
          Thank you to arXiv for use of its open access interoperability.
        </div>
      </body>
    </html>
  )
}
Enter fullscreen mode Exit fullscreen mode

loading.tsx

O componente de carregamento renderiza enquanto aguarda a promessa do componente da página.

// app/[route]/loading.tsx
export default function PageLoading() {
  return (
    <main className={styles.main}>
      <FeedLoader />
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

Um desafio interessante aqui, eu não consegui encontrar nenhum componente com uma animação giratória de código aberto que funcionasse. A animação não dispara 🤨.

Carregando dados

O carregamento de dados é o exemplo típico de uma operação lenta que requer promessas. Mas você pode usar as mesmas técnicas para qualquer coisa.

Como no React Query, a recomendação é carregar dados perto de onde são usados. No mesmo componente é melhor. Você pode pensar nisso como declarando uma dependência de dados em seu componente e permitindo que React e NextJS lidem com os detalhes.

Por exemplo, aqui está como eu carrego um feed arXiv:

// carrega lista de artigos
// renderiza em um loop
export const FeedInnards = async (props: FeedProps) => {
  const { offset = 0, count = 10 } = props

  let feed: arxiv.ArxivFeed
  let papers: arxiv.ArxivFeedItem[]

  try {
    feed = await arxiv.getFeed(props.topic)
    papers = feed.items.slice(offset, count)
  } catch (e) {
    console.error(e)

    return (
      <>
        <p>Error loading feed. Try one of these topics instead:</p>
        <TopicsList />
      </>
    )
  }

  return (
    <>
      {papers.map((paper) => (
        // @ts-expect-error Server Component
        <FeedItem paper={paper} key={paper.link} />
      ))}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Observe o await no corpo desse componente, esse é o novo brinquedo brilhante! React/NextJS mostra um estado de carregamento enquanto a promessa deste componente está pendente. Você não precisa lidar com isso 😍

Você precisa escrever sua própria lógica de try..catch, porque limites de erro não funcionam com componentes assíncronos. Ainda?

Busca estendida com cache

Os dados buscados no meu componente <FeedInnards> se parece com isso:

export async function getFeed(category: string): Promise<ArxivFeed> {
  const parser: Parser<Omit<ArxivFeed, "items">, ArxivFeedItem> = new Parser()
  const feed = await fetch(
    `http://export.arxiv.org/rss/${category}?version=1.0`,
    {
      next: { revalidate: TEN_HOURS },
    }
  ).then((r) => r.text())

  try {
    const parsed = await parser.parseString(feed)
    return parsed
  } catch (e) {
    throw new Error("Could not parse feed")
  }
}
Enter fullscreen mode Exit fullscreen mode

Busca o feed RSS do arXiv usando um padrão fetch() depois usa um analisador de RSS. Nada louco.

Mas observe os parâmetros extras nessa chamada de busca:

await fetch(`http://export.arxiv.org/rss/${category}?version=1.0`, {
  next: { revalidate: TEN_HOURS },
})
Enter fullscreen mode Exit fullscreen mode

NextJS adiciona um parâmetro personalizado para fetch() que permite especificar um comportamento de cache. Você pode ativar/desativar o cache e especificar o comportamento da revalidação.

No meu caso, o aplicativo busca um novo feed a cada 10 horas. Recarregue a página antes disso e você obterá um resultado estável sem indicadores de carregamento. O cache é estável entre os usuários e a maioria dos visitantes obtém uma página quase estática e rápida.

Não está claro para mim onde esse cache vive. Isso é algo que funciona com o NextJS ou apenas com a Vercel? 🤔

Cache sem fetch() ou libs de terceiros

Quando você não controla a chamada API subjacente (como em uma biblioteca), você pode armazenar em cache os resultados usando o novo método React.cache(). Útil para qualquer operação lenta, porque funciona em funções que retornam promessas, ao invés de alterar a operação do fetch() em si.

Por exemplo, quando estou usando o OpenAI para criar resumos:

const getSummary = cache(async (paper: arxiv.ArxivFeedItem) => {
  const summary = await openai.getSummary(paper)
  return summary
})

const PaperSummary = async (props: { paper: arxiv.ArxivFeedItem }) => {
  const summary = await getSummary(props.paper)

  return <p>{summary.choices[0].text}</p>
}
Enter fullscreen mode Exit fullscreen mode

Seguindo a idéia de que "você deve carregar dados perto de onde são usados", eu tenho um componente que recebe um paper, chama OpenAI para resumir e renderiza um único parágrafo.

A chamada OpenAI está envolvida em cache() para aumentar o desempenho. Para otimização de custos, uso adicionalmente o cache Redis na parte superior. Mais sobre isso outra hora :)

Usando React Suspense para carregamento personalizado

Os estados de carregamento no nível da página são ótimos, mas você pode querer um controle mais refinado. Ou para carregar componentes em paralelo com a renderização otimista de "renderiza o primeiro que chegar".

Você faz cria limites virtuais com o componente <Suspense>.

Por exemplo, quando você conhece a parte resumida de um <FeedItem> é mais lento que o resto:

const FeedItem = async (props: { paper: arxiv.ArxivFeedItem }) => {
  const { paper } = props

  // ...

  return (
    <div className={feedStyles.item}>
      // ...
      <Suspense fallback={<RingLoader color="blue" loading />}>
        {/* @ts-expect-error Server Component */}
        <PaperSummary paper={paper} />
      </Suspense>
      <div>
        Full paper at 👉{" "}
        <a href={paper.link} className={feedStyles.linkPaper}>
          {paper.title.split(/(\(|\. \()arXiv/)[0]}
        </a>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Usando o <Suspense> é como dizer ao React para "renderizar o componente <FeedItem>, mas mostre um fallback enquanto as promessas dentro desta parte da árvore componente estão pendentes".

Esses limites virtuais com React Suspense capturará todas as promessas em seus componentes filhos. Você não precisa coordenar nada.

Você pode aumentar os limites virtuais do React Suspense onde quer que faça sentido para o seu aplicativo. Até o estado de carregamento do nível da página é um limite virtual do React Suspense sob o capô.

Finalizando

Eu amo isso! O Async React simplificará muito do meu código.

Mas estou preocupado que isso seja difícil de implementar fora do NextJS e da Vercel. Veremos.

Saúde,

~ Swizec

PS: A documentação beta para o NextJS 13 está fantástica.


Créditos

💖 💪 🙅 🚩
oieduardorabelo
Eduardo Rabelo

Posted on January 26, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Async React com NextJS 13
webdev Async React com NextJS 13

January 26, 2023