Voltar para posts

Guia Completo: Melhores Práticas Next.js + shadcn/ui

Kaique Mitsuo Silva Yamamoto
0 visualizações

Guia Completo: Melhores Práticas Next.js + shadcn/ui

Índice

  1. Introdução

  2. Configuração Inicial

  3. Arquitetura e Organização

  4. Server Components vs Client Components

  5. Gerenciamento de Formulários

  6. Otimização de Performance

  7. Padrões de Composição

  8. Estilização e Temas

  9. Erros Comuns e Soluções

  10. Checklist de Boas Práticas


Introdução

O que é shadcn/ui?

shadcn/ui não é uma biblioteca de componentes tradicional, mas sim uma coleção de componentes reutilizáveis que você copia diretamente para seu projeto. Construído sobre Tailwind CSS e Radix UI, oferece componentes acessíveis e personalizáveis.

Por que Next.js + shadcn/ui?

  • Integração perfeita: shadcn/ui foi projetado pensando no Next.js App Router

  • Performance: Suporte nativo a Server Components

  • TypeScript: Type-safety completa

  • Customização: Controle total sobre o código dos componentes

  • Acessibilidade: Componentes baseados em Radix UI com ARIA adequado


Configuração Inicial

1. Criando um Novo Projeto

# Criar projeto Next.js com TypeScript
npx create-next-app@latest meu-projeto --typescript --tailwind --eslint --app

# Navegar para o diretório
cd meu-projeto

# Inicializar shadcn/ui
npx shadcn@latest init

2. Configuração do shadcn/ui

Durante a inicialização, você será questionado sobre:

  • Style: Default, New York, ou custom

  • Base color: Cor base para o tema

  • CSS variables: Recomendado para fácil customização

  • TypeScript: Sempre sim

  • Tailwind config: Usar CSS variables para flexibilidade

3. Estrutura de Paths

Certifique-se que seu tsconfig.json tem os paths corretos:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}

4. Importando o CSS Global

No app/layout.tsx:

import "./globals.css"

Arquitetura e Organização

Estrutura de Diretórios Recomendada

meu-projeto/
├── app/                          # App Router
│   ├── (auth)/                   # Route groups
│   │   ├── login/
│   │   └── register/
│   ├── (dashboard)/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── api/                      # Route Handlers
│   ├── layout.tsx                # Root layout
│   └── page.tsx                  # Home page
│
├── components/
│   ├── ui/                       # Componentes shadcn/ui
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   └── ...
│   ├── layout/                   # Componentes de layout
│   │   ├── header.tsx
│   │   ├── footer.tsx
│   │   └── sidebar.tsx
│   ├── forms/                    # Componentes de formulário
│   │   ├── login-form.tsx
│   │   └── user-form.tsx
│   ├── features/                 # Componentes específicos
│   │   ├── dashboard/
│   │   └── products/
│   └── shared/                   # Componentes reutilizáveis
│       ├── data-table.tsx
│       └── modal.tsx
│
├── lib/                          # Utilitários
│   ├── utils.ts                  # Função cn() e helpers
│   ├── validations.ts            # Schemas Zod
│   └── api.ts                    # Cliente API
│
├── hooks/                        # Custom hooks
│   ├── use-toast.ts
│   └── use-media-query.ts
│
├── types/                        # TypeScript types
│   ├── index.ts
│   └── api.ts
│
└── config/                       # Configurações
    ├── site.ts
    └── nav.ts

Princípios de Organização

Separar componentes UI da lógica de negócio mantém o projeto limpo e sustentável. Use a seguinte hierarquia:

  1. Componentes Base (components/ui/): Componentes shadcn/ui puros

  2. Componentes de Seção (components/features/): Combinam componentes base com lógica específica

  3. Páginas (app/): Mínimas, apenas estrutura e composição


Server Components vs Client Components

Regra de Ouro

Por padrão, layouts e páginas são Server Components, permitindo buscar dados e renderizar partes da UI no servidor.

Quando Usar Server Components

Use Server Components para:

  • Buscar dados de bancos de dados ou APIs

  • Acessar recursos backend diretamente

  • Manter informações sensíveis no servidor (tokens, chaves API)

  • Componentes que não precisam de interatividade

  • Reduzir o JavaScript enviado ao cliente

// app/dashboard/page.tsx
// Server Component por padrão - sem "use client"
export default async function DashboardPage() {
  // Buscar dados diretamente no servidor
  const data = await fetch('https://api.example.com/data', {
    cache: 'no-store' // Para dados dinâmicos
  }).then(res => res.json())
  
  return (
    <div>
      <h1>{data.title}</h1>
      {/* Passar dados para Client Components */}
      <InteractiveChart data={data.stats} />
    </div>
  )
}

Quando Usar Client Components

Use Client Components quando precisar de estado, event handlers, lifecycle ou APIs do navegador.

Use Client Components para:

  • Interatividade (onClick, onChange)

  • State e lifecycle hooks (useState, useEffect)

  • APIs do navegador (localStorage, window)

  • Componentes shadcn/ui interativos (Dialog, Dropdown, etc.)

  • Custom hooks

// components/features/interactive-chart.tsx
"use client" // Diretiva necessária

import { useState } from 'react'
import { Card } from '@/components/ui/card'

interface InteractiveChartProps {
  data: number[]
}

export function InteractiveChart({ data }: InteractiveChartProps) {
  const [selectedRange, setSelectedRange] = useState('7d')
  
  return (
    <Card>
      <button onClick={() => setSelectedRange('30d')}>
        Ver 30 dias
      </button>
      {/* Lógica de chart */}
    </Card>
  )
}

Padrão de Composição Ideal

Mantenha Client Components como folhas da árvore e passe dados dos Server Components pai como props.

// app/page.tsx (Server Component)
import { InteractiveButton } from '@/components/interactive-button'

export default async function Page() {
  const post = await getPost()
  
  return (
    <div>
      <main>
        <h1>{post.title}</h1>
        {/* Client Component recebe dados do Server Component */}
        <InteractiveButton likes={post.likes} />
      </main>
    </div>
  )
}

// components/interactive-button.tsx (Client Component)
"use client"

import { useState } from 'react'
import { Button } from '@/components/ui/button'

export function InteractiveButton({ likes }: { likes: number }) {
  const [count, setCount] = useState(likes)
  
  return (
    <Button onClick={() => setCount(count + 1)}>
      Curtir ({count})
    </Button>
  )
}

shadcn/ui e Server Components

Muitos componentes shadcn/ui funcionam perfeitamente como Server Components (Card, Badge, Separator), enquanto os interativos (Dialog, Dropdown Menu) são marcados com "use client" automaticamente.

Componentes que funcionam como Server Components:

  • Card, Badge, Separator

  • Avatar (sem interação)

  • Alert, AspectRatio

Componentes que precisam de "use client":

  • Dialog, Sheet, Drawer

  • Dropdown Menu, Select

  • Tooltip, Popover

  • Checkbox, Switch, Radio Group

  • Formulários com validação


Gerenciamento de Formulários

Stack Recomendada

shadcn/ui usa React Hook Form para gerenciamento de estado performático e Zod para validação type-safe.

# Instalar dependências
npm install react-hook-form zod @hookform/resolvers

# Adicionar componente Form do shadcn/ui
npx shadcn@latest add form

Estrutura Básica de Formulário

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

// 1. Definir schema de validação
const formSchema = z.object({
  username: z
    .string()
    .min(2, "Nome de usuário deve ter no mínimo 2 caracteres")
    .max(30, "Nome de usuário deve ter no máximo 30 caracteres"),
  email: z
    .string()
    .email("Email inválido"),
  password: z
    .string()
    .min(8, "Senha deve ter no mínimo 8 caracteres")
    .regex(/[A-Z]/, "Senha deve conter ao menos uma letra maiúscula")
    .regex(/[0-9]/, "Senha deve conter ao menos um número"),
})

// 2. Criar componente do formulário
export function UserForm() {
  // 3. Configurar form com resolver Zod
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      email: "",
      password: "",
    },
  })

  // 4. Handler de submissão
  async function onSubmit(values: z.infer<typeof formSchema>) {
    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(values),
      })
      
      if (response.ok) {
        // Sucesso
        console.log('Usuário criado!')
      }
    } catch (error) {
      console.error(error)
    }
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Nome de Usuário</FormLabel>
              <FormControl>
                <Input placeholder="joao.silva" {...field} />
              </FormControl>
              <FormDescription>
                Este será seu nome público de exibição.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input type="email" placeholder="joao@example.com" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Senha</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? "Enviando..." : "Criar Conta"}
        </Button>
      </form>
    </Form>
  )
}

Validação Avançada com Zod

// lib/validations.ts
import { z } from "zod"

// Validação customizada
export const customerSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().min(18, "Deve ser maior de idade"),
  city: z.string().min(2),
  country: z.string().length(2, "Use código de país de 2 letras"),
}).superRefine((data, ctx) => {
  // Validação customizada: não permitir espaços no nome
  if (data.name.includes(" ")) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Nome não pode conter espaços",
      path: ["name"],
    })
  }
  
  // Validação condicional
  if (data.country === "BR" && !["São Paulo", "Rio de Janeiro"].includes(data.city)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Cidade inválida para o país selecionado",
      path: ["city"],
    })
  }
})

// Schema com validação assíncrona (ex: verificar se email já existe)
export const registrationSchema = z.object({
  email: z.string().email(),
  username: z.string().min(3),
}).refine(
  async (data) => {
    // Verificar se username já existe
    const exists = await checkUsernameExists(data.username)
    return !exists
  },
  {
    message: "Nome de usuário já está em uso",
    path: ["username"],
  }
)

Melhores Práticas para Formulários

Sempre complemente validação client-side com validação server-side para garantir integridade dos dados.

  1. Separe schemas de validação em arquivos dedicados (lib/validations.ts)

  2. Use TypeScript para inferir tipos do schema Zod

  3. Forneça mensagens de erro claras e acionáveis

  4. Implemente validação em tempo real mas de forma inteligente (não em cada keystroke)

  5. Sempre valide no servidor mesmo com validação client-side

  6. Use defaultValues para evitar componentes não controlados

  7. Desabilite o botão de submit durante o envio


Otimização de Performance

1. Prefetching e Caching

Next.js automaticamente faz prefetch de rotas linkadas com o componente Link quando entram no viewport.

import Link from 'next/link'

export function Navigation() {
  return (
    <nav>
      {/* Prefetch automático */}
      <Link href="/blog">Blog</Link>
      
      {/* Desabilitar prefetch se necessário */}
      <Link href="/contact" prefetch={false}>
        Contato
      </Link>
    </nav>
  )
}

2. Streaming com Suspense

Streaming permite renderizar progressivamente UI do servidor, mostrando partes da página imediatamente antes do conteúdo completo terminar.

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'

// Componente que busca dados lentos
async function RecentSales() {
  // Simular busca lenta
  const data = await fetch('https://api.example.com/sales', {
    cache: 'no-store'
  })
  
  return (
    <Card>
      <CardHeader>
        <CardTitle>Vendas Recentes</CardTitle>
      </CardHeader>
      <CardContent>
        {/* Renderizar dados */}
      </CardContent>
    </Card>
  )
}

// Loading component
function RecentSalesLoading() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Vendas Recentes</CardTitle>
      </CardHeader>
      <CardContent>
        <Skeleton className="h-32 w-full" />
      </CardContent>
    </Card>
  )
}

export default function DashboardPage() {
  return (
    <div className="grid gap-4">
      {/* Conteúdo que carrega rápido */}
      <Card>
        <CardHeader>
          <CardTitle>Dashboard</CardTitle>
        </CardHeader>
      </Card>
      
      {/* Conteúdo lento com Suspense */}
      <Suspense fallback={<RecentSalesLoading />}>
        <RecentSales />
      </Suspense>
    </div>
  )
}

3. Evitar Componentes Client Desnecessários

Evite renderização client-side desnecessária para melhorar performance - use Server Components onde possível.

❌ Errado: Buscar dados no Client Component

"use client"

import { useEffect, useState } from 'react'

export function UserList() {
  const [users, setUsers] = useState([])
  
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(setUsers)
  }, [])
  
  // Renderizar...
}

✅ Correto: Buscar dados no Server Component

// Server Component
export default async function UserList() {
  const users = await fetch('https://api.example.com/users')
    .then(res => res.json())
  
  return (
    <div>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  )
}

4. Otimizar Importações de Componentes

// ❌ Evite: importar tudo
import * as React from 'react'

// ✅ Prefira: importações nomeadas
import { useState, useEffect } from 'react'

// Para componentes shadcn/ui
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle } from '@/components/ui/card'

5. Usar loading.tsx para Melhor UX

// app/dashboard/loading.tsx
import { Skeleton } from '@/components/ui/skeleton'

export default function DashboardLoading() {
  return (
    <div className="grid gap-4">
      <Skeleton className="h-32 w-full" />
      <Skeleton className="h-64 w-full" />
    </div>
  )
}

6. Memoização Inteligente

"use client"

import { useMemo, useCallback } from 'react'
import { Card } from '@/components/ui/card'

export function ExpensiveComponent({ data }: { data: Item[] }) {
  // Memoizar cálculos caros
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      computed: expensiveCalculation(item)
    }))
  }, [data])
  
  // Memoizar callbacks
  const handleClick = useCallback((id: string) => {
    console.log('Clicked:', id)
  }, [])
  
  return (
    <div>
      {processedData.map(item => (
        <Card key={item.id} onClick={() => handleClick(item.id)}>
          {item.computed}
        </Card>
      ))}
    </div>
  )
}

Padrões de Composição

1. Padrão de Wrapper para Providers

Para permitir que Server e Client Components se intercalem, é importante fazer seu provider (ou múltiplos providers) serem Client Components separados que recebem children como prop.

// components/providers.tsx
"use client"

import { ThemeProvider } from "next-themes"

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
      disableTransitionOnChange
    >
      {children}
    </ThemeProvider>
  )
}

// app/layout.tsx (Server Component)
import { Providers } from '@/components/providers'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pt-BR" suppressHydrationWarning>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

2. Padrão de Composição com Slots

// components/ui/card-with-slots.tsx
import { Card, CardHeader, CardTitle, CardContent } from './card'

interface CardWithSlotsProps {
  title: string
  header?: React.ReactNode
  footer?: React.ReactNode
  children: React.ReactNode
}

export function CardWithSlots({
  title,
  header,
  footer,
  children
}: CardWithSlotsProps) {
  return (
    <Card>
      <CardHeader>
        {header}
        <CardTitle>{title}</CardTitle>
      </CardHeader>
      <CardContent>
        {children}
      </CardContent>
      {footer && <div className="px-6 py-4">{footer}</div>}
    </Card>
  )
}

// Uso
<CardWithSlots 
  title="Dashboard"
  header={<Badge>Novo</Badge>}
  footer={<Button>Ver mais</Button>}
>
  Conteúdo do card
</CardWithSlots>

3. Padrão de Variantes com CVA

// components/ui/custom-button.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "./button"

const customButtonVariants = cva(
  "transition-all duration-200",
  {
    variants: {
      variant: {
        primary: "bg-blue-600 hover:bg-blue-700",
        secondary: "bg-gray-600 hover:bg-gray-700",
        success: "bg-green-600 hover:bg-green-700",
        danger: "bg-red-600 hover:bg-red-700",
      },
      size: {
        sm: "h-8 px-3 text-xs",
        md: "h-10 px-4 text-sm",
        lg: "h-12 px-6 text-base",
      },
    },
    defaultVariants: {
      variant: "primary",
      size: "md",
    },
  }
)

interface CustomButtonProps 
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof customButtonVariants> {
  children: React.ReactNode
}

export function CustomButton({ 
  variant, 
  size, 
  className,
  children,
  ...props 
}: CustomButtonProps) {
  return (
    <Button
      className={cn(customButtonVariants({ variant, size }), className)}
      {...props}
    >
      {children}
    </Button>
  )
}

// Uso
<CustomButton variant="success" size="lg">
  Salvar
</CustomButton>

4. Padrão Compound Components

// components/ui/stat-card.tsx
import { Card, CardHeader, CardTitle, CardContent } from './card'
import { cn } from '@/lib/utils'

interface StatCardProps {
  children: React.ReactNode
  className?: string
}

export function StatCard({ children, className }: StatCardProps) {
  return (
    <Card className={cn("p-6", className)}>
      {children}
    </Card>
  )
}

StatCard.Title = function StatCardTitle({ 
  children, 
  className 
}: { 
  children: React.ReactNode
  className?: string 
}) {
  return (
    <h3 className={cn("text-sm font-medium text-muted-foreground", className)}>
      {children}
    </h3>
  )
}

StatCard.Value = function StatCardValue({ 
  children, 
  className 
}: { 
  children: React.ReactNode
  className?: string 
}) {
  return (
    <p className={cn("text-3xl font-bold", className)}>
      {children}
    </p>
  )
}

StatCard.Description = function StatCardDescription({ 
  children, 
  className 
}: { 
  children: React.ReactNode
  className?: string 
}) {
  return (
    <p className={cn("text-xs text-muted-foreground", className)}>
      {children}
    </p>
  )
}

// Uso
<StatCard>
  <StatCard.Title>Total de Vendas</StatCard.Title>
  <StatCard.Value>$45,231.89</StatCard.Value>
  <StatCard.Description>+20.1% em relação ao mês anterior</StatCard.Description>
</StatCard>

Estilização e Temas

1. Configuração de Tema Dark/Light

shadcn/ui oferece suporte a modo escuro pronto para uso com next-themes.

npm install next-themes
// components/theme-provider.tsx
"use client"

import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

// components/theme-toggle.tsx
"use client"

import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"

export function ThemeToggle() {
  const { setTheme, theme } = useTheme()

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Alternar tema</span>
    </Button>
  )
}

// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

2. Customizar Cores do Tema

Em app/globals.css:

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 221.2 83.2% 53.3%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 47.4% 11.2%;
    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 224.3 76.3% 48%;
  }
}

3. Utility Classes Customizadas

Mantenha estilos mínimos e orientados a utility - use classes Tailwind CSS em vez de estilos inline.

// ❌ Evite estilos inline
<div style={{ padding: '1rem', backgroundColor: '#blue' }}>
  Conteúdo
</div>

// ✅ Use utility classes
<div className="p-4 bg-blue-600">
  Conteúdo
</div>

// ✅ Combine com cn() para condicional
<div className={cn(
  "p-4 rounded-lg",
  isActive && "bg-blue-600",
  isError && "bg-red-600"
)}>
  Conteúdo
</div>

4. Criar Componentes Estilizados Reutilizáveis

// components/ui/section.tsx
import { cn } from "@/lib/utils"

interface SectionProps extends React.HTMLAttributes<HTMLElement> {
  children: React.ReactNode
}

export function Section({ className, children, ...props }: SectionProps) {
  return (
    <section
      className={cn(
        "container mx-auto px-4 py-12 md:py-16 lg:py-20",
        className
      )}
      {...props}
    >
      {children}
    </section>
  )
}

// components/ui/heading.tsx
import { cn } from "@/lib/utils"

interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
  as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
  children: React.ReactNode
}

export function Heading({ 
  as: Tag = 'h2', 
  className, 
  children, 
  ...props 
}: HeadingProps) {
  return (
    <Tag
      className={cn(
        "scroll-m-20 font-bold tracking-tight",
        Tag === 'h1' && "text-4xl lg:text-5xl",
        Tag === 'h2' && "text-3xl lg:text-4xl",
        Tag === 'h3' && "text-2xl lg:text-3xl",
        Tag === 'h4' && "text-xl lg:text-2xl",
        className
      )}
      {...props}
    >
      {children}
    </Tag>
  )
}

Erros Comuns e Soluções

1. Usar APIs do Navegador em Server Components

❌ Erro:

// Server Component
export default function Page() {
  const data = localStorage.getItem('user') // ❌ Erro!
  return <div>{data}</div>
}

✅ Solução:

// Client Component
"use client"

import { useEffect, useState } from 'react'

export default function Page() {
  const [data, setData] = useState<string | null>(null)
  
  useEffect(() => {
    setData(localStorage.getItem('user'))
  }, [])
  
  return <div>{data}</div>
}

2. Esquecer "use client" em Componentes Interativos

Quando a diretiva "use client" é adicionada, você passa para o "client boundary" dando a habilidade de executar JavaScript client-side.

❌ Erro:

import { useState } from 'react' // ❌ Erro sem "use client"

export function Counter() {
  const [count, setCount] = useState(0)
  // ...
}

✅ Solução:

"use client" // ✅ Adicionar diretiva

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  // ...
}

3. Buscar Dados em Client Component Desnecessariamente

Buscar dados no client tem sido prática comum no React por anos, mas não é tão eficiente devido à latência de rede adicional.

❌ Evite:

"use client"

export function UserProfile() {
  const [user, setUser] = useState(null)
  
  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(setUser)
  }, [])
  
  // ...
}

✅ Prefira:

// Server Component
export default async function UserProfile() {
  const user = await fetch('https://api.example.com/user')
    .then(res => res.json())
  
  return <UserCard user={user} />
}

4. Não Revalidar Dados Após Mutações

Um erro comum é esquecer de revalidar dados após uma mutação.

✅ Solução com Server Actions:

// app/actions.ts
"use server"

import { revalidatePath, revalidateTag } from 'next/cache'

export async function createPost(formData: FormData) {
  // Criar post...
  
  // Revalidar rota específica
  revalidatePath('/blog')
  
  // Ou revalidar por tag
  revalidateTag('posts')
}

// Client Component
"use client"

import { createPost } from '@/app/actions'

export function CreatePostForm() {
  return (
    <form action={createPost}>
      {/* campos do formulário */}
      <button type="submit">Criar Post</button>
    </form>
  )
}

5. Paths TypeScript Não Configurados

Problema:

import { Button } from '../../../../components/ui/button' // ❌ Feio!

Solução em tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}

Uso:

import { Button } from '@/components/ui/button' // ✅ Limpo!

6. Usar Context Incorretamente com Server Components

❌ Erro:

// Server Component tentando usar Context
import { useContext } from 'react' // ❌ Não funciona!
import { ThemeContext } from './theme-context'

export default function Page() {
  const theme = useContext(ThemeContext) // ❌ Erro!
  // ...
}

✅ Solução:

// components/theme-provider.tsx
"use client"

import { createContext, useContext } from 'react'

const ThemeContext = createContext<Theme | undefined>(undefined)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  // provider logic
  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return context
}

// app/layout.tsx (Server Component)
import { ThemeProvider } from '@/components/theme-provider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

7. Não Usar Suspense para Dados Assíncronos

❌ Sem Suspense:

export default async function Page() {
  const [data1, data2, data3] = await Promise.all([
    fetchData1(),
    fetchData2(),
    fetchData3()
  ])
  
  // Usuário espera TUDO carregar antes de ver qualquer coisa
  return (
    <div>
      <Section1 data={data1} />
      <Section2 data={data2} />
      <Section3 data={data3} />
    </div>
  )
}

✅ Com Suspense:

import { Suspense } from 'react'

export default function Page() {
  return (
    <div>
      {/* Mostra Section1 assim que carrega */}
      <Suspense fallback={<Section1Loading />}>
        <Section1 />
      </Suspense>
      
      {/* Mostra Section2 independentemente */}
      <Suspense fallback={<Section2Loading />}>
        <Section2 />
      </Suspense>
      
      {/* Mostra Section3 independentemente */}
      <Suspense fallback={<Section3Loading />}>
        <Section3 />
      </Suspense>
    </div>
  )
}

async function Section1() {
  const data = await fetchData1()
  return <div>{/* render data */}</div>
}

Checklist de Boas Práticas

🏗️ Arquitetura

  • Estrutura de pastas clara e escalável

  • Separação entre Server e Client Components

  • Componentes shadcn/ui em components/ui/

  • Componentes de negócio organizados por feature

  • Utilitários centralizados em lib/

  • Types TypeScript em arquivos dedicados

⚡ Performance

  • Maximizar uso de Server Components

  • Client Components apenas quando necessário

  • Usar Suspense para dados assíncronos

  • Implementar loading.tsx para rotas dinâmicas

  • Prefetch inteligente com Link

  • Memoização adequada (useMemo, useCallback)

  • Otimizar importações (tree-shaking)

📝 Formulários

  • React Hook Form para gerenciamento de estado

  • Zod para validação type-safe

  • Schemas de validação separados

  • Mensagens de erro claras

  • Validação server-side obrigatória

  • Feedback visual durante submissão

  • Acessibilidade (labels, ARIA)

🎨 Estilização

  • CSS variables para temas configurados

  • Dark mode implementado com next-themes

  • Utility-first com Tailwind CSS

  • Evitar estilos inline

  • Usar função cn() para classes condicionais

  • Componentes estilizados reutilizáveis

  • Variantes com CVA quando apropriado

🔒 Segurança e Boas Práticas

  • Dados sensíveis apenas no servidor

  • Validação client + server-side

  • Headers e cookies via Server Components

  • Revalidação após mutações

  • Error boundaries implementados

  • Loading states para UX

  • TypeScript strict mode ativado

♿ Acessibilidade

  • Componentes shadcn/ui com ARIA adequado

  • Labels em todos os inputs

  • Navegação por teclado funcional

  • Contraste de cores adequado

  • Focus visible em interações

  • Screen reader friendly

  • Semantic HTML

🧪 Qualidade de Código

  • ESLint configurado e sem warnings

  • Prettier para formatação

  • Componentes pequenos e focados

  • Props com tipos TypeScript

  • Comentários em lógica complexa

  • Naming consistente

  • DRY (Don't Repeat Yourself)

🚀 Deploy e Produção

  • Build sem erros

  • Configurações de ambiente corretas

  • Otimização de imagens (next/image)

  • Meta tags para SEO

  • Sitemap configurado

  • Analytics implementado

  • Error tracking (Sentry, etc.)


Recursos Adicionais

Documentação Oficial

Ferramentas Úteis

Templates e Starters


Conclusão

A combinação de Next.js com shadcn/ui oferece uma stack moderna, performática e altamente customizável para desenvolvimento web. Seguindo as práticas apresentadas neste guia, você conseguirá:

✅ Construir aplicações escaláveis e manuteníveis
✅ Maximizar performance com Server Components
✅ Criar UIs acessíveis e bonitas
✅ Implementar formulários robustos com validação type-safe
✅ Manter código limpo e organizado

Lembre-se: estas são diretrizes, não regras absolutas. Adapte conforme as necessidades do seu projeto, mas sempre priorizando performance, acessibilidade e experiência do usuário.


Última atualização: Dezembro 2024
Versões: Next.js 15, React 19, shadcn/ui latest

Gostou do conteúdo?

Vamos conversar! Entre em contato através do WhatsApp ou conecte-se comigo no LinkedIn.