Remix v2 - Framework React Full-stack

2024.12.25

Visão Geral do Remix v2

Remix v2 é um framework React full-stack que aproveita as tecnologias fundamentais da web (HTTP, formulários, cache do navegador). O v2 traz integração com Vite, melhor segurança de tipos e melhorias de performance.

Integração com Vite

Configuração

// vite.config.ts
import { vitePlugin as remix } from '@remix-run/dev';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    remix({
      future: {
        v3_fetcherPersist: true,
        v3_relativeSplatPath: true
      }
    })
  ]
});

Servidor de Desenvolvimento

npm run dev
# HMR rápido do Vite disponível

Roteamento Baseado em Arquivos

app/
├── routes/
│   ├── _index.tsx           # /
│   ├── about.tsx            # /about
│   ├── posts._index.tsx     # /posts
│   ├── posts.$postId.tsx    # /posts/:postId
│   ├── posts.$postId_.edit.tsx # /posts/:postId/edit
│   └── ($lang).docs.$.tsx   # /:lang?/docs/*
└── root.tsx

Convenções de Nomenclatura de Rotas

PadrãoSignificado
_indexRota índice
$paramParâmetro dinâmico
($param)Parâmetro opcional
$Splat (catch-all)
_Rota sem layout
.URL aninhada

Carregamento de Dados

loader

// routes/posts.$postId.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await db.post.findUnique({
    where: { id: params.postId }
  });

  if (!post) {
    throw new Response('Não encontrado', { status: 404 });
  }

  return json({ post });
}

export default function Post() {
  const { post } = useLoaderData<typeof loader>();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

action

// routes/posts.new.tsx
import { redirect, type ActionFunctionArgs } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
import { z } from 'zod';

const schema = z.object({
  title: z.string().min(1),
  content: z.string().min(10)
});

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const result = schema.safeParse(Object.fromEntries(formData));

  if (!result.success) {
    return json({ errors: result.error.flatten() }, { status: 400 });
  }

  const post = await db.post.create({ data: result.data });
  return redirect(`/posts/${post.id}`);
}

export default function NewPost() {
  const actionData = useActionData<typeof action>();

  return (
    <Form method="post">
      <input name="title" />
      {actionData?.errors?.fieldErrors?.title && (
        <span>{actionData.errors.fieldErrors.title}</span>
      )}
      <textarea name="content" />
      <button type="submit">Criar</button>
    </Form>
  );
}

fetcher

Envia e obtém dados sem navegação.

import { useFetcher } from '@remix-run/react';

export default function LikeButton({ postId }: { postId: string }) {
  const fetcher = useFetcher();
  const isLiking = fetcher.state === 'submitting';

  return (
    <fetcher.Form method="post" action="/api/like">
      <input type="hidden" name="postId" value={postId} />
      <button type="submit" disabled={isLiking}>
        {isLiking ? 'Curtindo...' : 'Curtir'}
      </button>
    </fetcher.Form>
  );
}

defer e Suspense

import { defer } from '@remix-run/node';
import { Await, useLoaderData } from '@remix-run/react';
import { Suspense } from 'react';

export async function loader() {
  // Dados necessários imediatamente
  const user = await getUser();

  // Dados para carregamento adiado
  const recommendations = getRecommendations(); // sem await

  return defer({
    user,
    recommendations
  });
}

export default function Dashboard() {
  const { user, recommendations } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>Bem-vindo, {user.name}</h1>

      <Suspense fallback={<div>Carregando recomendações...</div>}>
        <Await resolve={recommendations}>
          {(data) => <RecommendationsList items={data} />}
        </Await>
      </Suspense>
    </div>
  );
}

Tratamento de Erros

// routes/posts.$postId.tsx
import { isRouteErrorResponse, useRouteError } from '@remix-run/react';

export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>Erro</h1>
      <p>{error instanceof Error ? error.message : 'Erro desconhecido'}</p>
    </div>
  );
}

Metadados

import { type MetaFunction } from '@remix-run/node';

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: data?.post.title ?? 'Não Encontrado' },
    { name: 'description', content: data?.post.excerpt },
    { property: 'og:title', content: data?.post.title }
  ];
};

Deploy

# Node.js
npm run build
npm start

# Cloudflare Pages
npm run build
npx wrangler pages deploy ./build/client

# Vercel
npm run build
# Configurar em vercel.json

Resumo

Remix v2 é um framework full-stack robusto que aproveita as tecnologias fundamentais da web. Com a melhoria da experiência de desenvolvimento através da integração com Vite, carregamento de dados type-safe e progressive enhancement, você pode construir aplicações web modernas de forma eficiente.

← Voltar para a lista