Guia Completo: Melhores Práticas Next.js + shadcn/ui
Guia Completo: Melhores Práticas Next.js + shadcn/ui
Índice
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:
Componentes Base (
components/ui/): Componentes shadcn/ui purosComponentes de Seção (
components/features/): Combinam componentes base com lógica específicaPá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.
Separe schemas de validação em arquivos dedicados (
lib/validations.ts)Use TypeScript para inferir tipos do schema Zod
Forneça mensagens de erro claras e acionáveis
Implemente validação em tempo real mas de forma inteligente (não em cada keystroke)
Sempre valide no servidor mesmo com validação client-side
Use
defaultValuespara evitar componentes não controladosDesabilite 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 condicionaisComponentes 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
shadcn Blocks - Blocos pré-construídos
v0.dev - Gerar UI com IA
Tailwind Variants - Alternativa ao CVA
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