Fundamentos do Next.js 15
Padrões de Comunicação entre Server e Client Components
Aprenda sobre padrões de comunicação entre server e client components
Padrões de Comunicação entre Server e Client Components 🤝
Bem-vindos à aula sobre os padrões de comunicação entre Server e Client Components no Next.js 15! 🚀
1. Introdução: O Diálogo entre Mundos Distintos
No Next.js 15, a arquitetura de Server Components e Client Components nos permite construir aplicações web altamente performáticas e interativas. No entanto, como componentes que vivem em ambientes diferentes (servidor vs. navegador), eles precisam de mecanismos claros e seguros para se comunicarem.
Esta aula explorará os padrões essenciais para que Server Components possam renderizar e passar dados para Client Components, e como Client Components podem "conversar de volta" com o servidor para realizar ações.
Vamos entender como esses dois mundos interagem de forma harmoniosa! ✨
2. Explicação Detalhada com Exemplos
2.1 Server Components Renderizando Client Components
O padrão mais fundamental é que Server Components podem importar e renderizar Client Components. Isso permite que você construa a maior parte da sua UI no servidor e "hidrate" partes interativas específicas com Client Components.
Quando um Server Component renderiza um Client Component, ele passa dados para ele através de props. Esses props devem ser serializáveis, pois eles serão enviados do servidor para o navegador.
Fluxo de Dados:
- Um Server Component (SC) busca dados no servidor.
- O SC renderiza um Client Component (CC), passando os dados como props.
- O Next.js serializa esses props.
- O CC é enviado ao navegador com os props serializados.
- No navegador, o CC é hidratado e pode usar os props para renderizar sua UI interativa.
Exemplo: Dashboard com Gráfico Interativo
Imagine uma página de dashboard que busca dados de vendas no servidor e exibe um gráfico interativo. A lógica de busca de dados fica no Server Component, e a interatividade do gráfico no Client Component.
// app/dashboard/page.js (Server Component)
import { getSalesData } from '../../lib/data'; // Função que busca dados no servidor
import SalesChart from './SalesChart'; // Client Component
export default async function DashboardPage() {
const salesData = await getSalesData(); // Dados buscados no servidor
return (
<div className="container">
<h1>Relatório de Vendas</h1>
<p>Dados atualizados até {new Date().toLocaleDateString()}.</p>
{/* O Server Component renderiza o Client Component e passa os dados como prop */}
<SalesChart data={salesData} />
</div>
);
}// app/dashboard/SalesChart.js (Client Component)
'use client'; // Marca este arquivo como um Client Component
import { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
export default function SalesChart({ data }) {
const [chartData, setChartData] = useState([]);
useEffect(() => {
// Aqui você pode fazer alguma transformação ou usar os dados diretamente
setChartData(data.map(item => ({ ...item, date: new Date(item.date).toLocaleDateString() })));
}, [data]);
return (
<div style={{ width: '100%', height: 300 }}>
<ResponsiveContainer>
<LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="sales" stroke="#8884d8" activeDot={{ r: 8 }} />
</LineChart>
</ResponsiveContainer>
<p>Clique nos pontos para ver detalhes!</p>
</div>
);
}Neste exemplo, o DashboardPage (SC) busca os salesData e os passa para o SalesChart (CC). O SalesChart recebe esses dados como prop e os utiliza para renderizar um gráfico interativo no navegador.
2.2 Client Components Utilizando Server Actions
E se um Client Component precisar enviar dados de volta para o servidor ou disparar uma ação no servidor (como salvar um item, atualizar um perfil, etc.)? É aqui que entram as Server Actions! 🚀
Server Actions são funções assíncronas que são executadas exclusivamente no servidor. Elas podem ser definidas diretamente em Server Components, em arquivos separados marcados com 'use server', ou passadas como props de Server Components para Client Components.
Exemplo: Formulário de Adição de Tarefas
Vamos criar um formulário em um Client Component que adiciona uma nova tarefa ao banco de dados usando uma Server Action.
// app/todos/actions.js (Server Action File)
'use server'; // Marca todo o arquivo como server-only
import { revalidatePath } from 'next/cache';
import { saveTodo } from '../../lib/db'; // Função simulada para salvar no DB
export async function createTodo(formData) {
const title = formData.get('title');
if (!title) {
return { error: 'O título da tarefa não pode ser vazio.' };
}
// Lógica de validação e salvamento no servidor
await saveTodo({ title, completed: false });
console.log(`Tarefa '${title}' salva no servidor!`);
// Revalida o cache para que a lista de tarefas seja atualizada
revalidatePath('/todos');
return { success: true, message: `Tarefa '${title}' adicionada!` };
}// app/todos/AddTodoForm.js (Client Component)
'use client'; // Marca este arquivo como um Client Component
import { useRef, useState } from 'react';
import { createTodo } from './actions'; // Importa a Server Action
export default function AddTodoForm() {
const formRef = useRef(null);
const [message, setMessage] = useState('');
const [isPending, setIsPending] = useState(false);
// Ação assíncrona que será disparada pelo formulário
const handleSubmit = async (event) => {
event.preventDefault(); // Evita o recarregamento padrão da página
setIsPending(true);
const formData = new FormData(formRef.current);
const result = await createTodo(formData); // Chama a Server Action!
if (result.error) {
setMessage(`Erro: ${result.error}`);
} else {
setMessage(result.message);
formRef.current.reset(); // Limpa o formulário
}
setIsPending(false);
};
return (
<form ref={formRef} onSubmit={handleSubmit} className="p-4 border rounded-md shadow-sm">
<input
type="text"
name="title"
placeholder="Adicionar nova tarefa..."
className="border p-2 rounded-md w-full mb-2"
disabled={isPending}
/>
<button
type="submit"
className="bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600 disabled:opacity-50"
disabled={isPending}
>
{isPending ? 'Adicionando...' : 'Adicionar Tarefa'}
</button>
{message && <p className="mt-2 text-sm">{message}</p>}
</form>
);
}Neste exemplo, o AddTodoForm (CC) importa e chama diretamente a createTodo Server Action. O Next.js se encarrega de fazer a chamada RPC (Remote Procedure Call) para o servidor, executar a função e retornar o resultado para o Client Component.
2.3 Passando Props de Server para Client: Regras de Serialização
Como mencionado, os props passados de um Server Component para um Client Component devem ser serializáveis. Isso significa que eles podem ser convertidos em uma string (geralmente JSON) e depois reconstruídos no lado do cliente.
O que é serializável (e pode ser passado como prop):
- Tipos primitivos:
string,number,boolean,null,undefined. - Objetos simples (plain objects):
{ key: value }ondevaluetambém é serializável. - Arrays:
[item1, item2]ondeitemtambém é serializável. - Funções de Server Actions: Sim! Uma Server Action pode ser passada como prop para um Client Component.
O que NÃO é serializável (e NÃO pode ser passado como prop):
- Funções JavaScript comuns (que não são Server Actions).
- Instâncias de classes (e.g.,
new Date(),new Map(),new Set()). - Símbolos (
Symbol). - Elementos JSX (um Server Component não pode passar um
<div>como prop para um Client Component; ele deve renderizá-lo diretamente ou passar dados para o CC renderizar seu próprio JSX).
Exemplo: Props Serializáveis e Não Serializáveis
// app/product/[id]/page.js (Server Component)
import ProductDetails from './ProductDetails'; // Client Component
// Função de exemplo para simular uma Server Action
async function updateProductRating(productId, rating) {
'use server';
console.log(`Atualizando rating do produto ${productId} para ${rating}`);
// Lógica para atualizar no DB
return { success: true };
}
export default function ProductPage({ params }) {
const productId = params.id;
const product = {
id: productId,
name: `Produto ${productId}`,
price: 99.99,
description: 'Um produto incrível.',
// dataCriacao: new Date(), // ❌ NÃO serializável!
// metodoExemplo: () => console.log('Olá'), // ❌ NÃO serializável!
};
return (
<div>
<h1>Detalhes do Produto</h1>
<ProductDetails
product={product}
// ✅ Server Action é serializável e pode ser passada como prop
onRateProduct={(rating) => updateProductRating(productId, rating)}
/>
</div>
);
}// app/product/[id]/ProductDetails.js (Client Component)
'use client';
import { useState } from 'react';
export default function ProductDetails({ product, onRateProduct }) {
const [rating, setRating] = useState(0);
const handleRate = async () => {
if (rating > 0) {
const result = await onRateProduct(rating); // Chama a Server Action!
if (result.success) {
alert('Avaliação enviada com sucesso!');
} else {
alert('Erro ao enviar avaliação.');
}
}
};
return (
<div className="p-4 border rounded-md shadow-sm">
<h2 className="text-xl font-bold">{product.name}</h2>
<p>Preço: R$ {product.price.toFixed(2)}</p>
<p>{product.description}</p>
{/* <p>Criado em: {product.dataCriacao.toLocaleDateString()}</p> */} {/* ❌ Isso falharia! */}
<div className="mt-4">
<label htmlFor="rating" className="block text-sm font-medium text-gray-700">
Avalie o produto:
</label>
<select
id="rating"
value={rating}
onChange={(e) => setRating(parseInt(e.target.value))}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
>
<option value="0">Selecione...</option>
<option value="1">1 Estrela</option>
<option value="2">2 Estrelas</option>
<option value="3">3 Estrelas</option>
<option value="4">4 Estrelas</option>
<option value="5">5 Estrelas</option>
</select>
<button
onClick={handleRate}
className="mt-2 bg-green-500 text-white p-2 rounded-md hover:bg-green-600"
disabled={rating === 0}
>
Enviar Avaliação
</button>
</div>
</div>
);
}2.4 Passando Server Actions como Props
Este é um padrão poderoso que combina os dois conceitos anteriores. Um Server Component pode definir uma Server Action (ou importá-la de um arquivo server-only) e passá-la como uma prop para um Client Component.
Isso permite que o Client Component dispare lógica de servidor sem precisar definir a Server Action diretamente dentro de si, mantendo a responsabilidade da ação no Server Component que a possui.
Exemplo: Botão de Deletar em uma Lista de Itens (inspirado na documentação oficial)
// app/items/actions.js (Server Action File)
'use server';
import { revalidatePath } from 'next/cache';
import { deleteItemFromDB } from '../../lib/db'; // Simula a exclusão no DB
export async function deleteItem(id) {
await deleteItemFromDB(id);
revalidatePath('/items'); // Revalida a lista de itens
console.log(`Item ${id} deletado.`);
return { success: true };
}// app/items/page.js (Server Component)
import { getItems } from '../../lib/db'; // Simula a busca de itens
import DeleteButton from './DeleteButton'; // Client Component
import { deleteItem } from './actions'; // Importa a Server Action
export default async function ItemsPage() {
const items = await getItems();
return (
<div className="container p-4">
<h1 className="text-2xl font-bold mb-4">Meus Itens</h1>
<ul className="space-y-2">
{items.map((item) => (
<li key={item.id} className="flex items-center justify-between p-3 border rounded-md shadow-sm">
<span>{item.name}</span>
{/* Passa a Server Action 'deleteItem' como prop para o Client Component */}
<DeleteButton itemId={item.id} onDelete={deleteItem} />
</li>
))}
</ul>
</div>
);
}// app/items/DeleteButton.js (Client Component)
'use client';
import { useState } from 'react';
export default function DeleteButton({ itemId, onDelete }) {
const [isDeleting, setIsDeleting] = useState(false);
const handleDelete = async () => {
if (window.confirm(`Tem certeza que deseja deletar o item ${itemId}?`)) {
setIsDeleting(true);
await onDelete(itemId); // Chama a Server Action passada como prop!
setIsDeleting(false);
}
};
return (
<button
onClick={handleDelete}
className="bg-red-500 text-white px-3 py-1 rounded-md hover:bg-red-600 disabled:opacity-50"
disabled={isDeleting}
>
{isDeleting ? 'Deletando...' : 'Deletar'}
</button>
);
}Neste exemplo, o ItemsPage (SC) busca os itens e para cada item, renderiza um DeleteButton (CC). Ele passa a Server Action deleteItem como prop onDelete para o botão. O DeleteButton (CC) então, ao ser clicado, invoca essa Server Action, que executa no servidor, deletando o item e revalidando o cache.
3. Exercícios/Desafios (Teóricos) 🧠
Para consolidar seu entendimento, reflita sobre os seguintes cenários:
-
Cenário 1: Blog com Comentários
- Você tem uma página de post de blog que é um Server Component.
- Os comentários para o post são exibidos por um Client Component
CommentList. - Há um formulário para adicionar um novo comentário, que é um Client Component
AddCommentForm. - Pergunta: Descreva o fluxo de dados e ações para:
- a) O
CommentListexibir os comentários iniciais. - b) O
AddCommentFormenviar um novo comentário para o servidor.
- a) O
-
Cenário 2: Perfil de Usuário com Edição
- A página de perfil de usuário é um Server Component
UserProfilePageque busca os dados do usuário. - Um Client Component
EditProfileFormpermite ao usuário editar seu nome e e-mail. - Pergunta: Como você passaria a função para salvar as alterações do
EditProfileFormde volta para o servidor, garantindo que oUserProfilePageseja atualizado após a edição?
- A página de perfil de usuário é um Server Component
-
Cenário 3: Problema de Serialização
- Um Server Component tenta passar um objeto
user = { name: "Alice", lastLogin: new Date() }para um Client Component. - Pergunta: O que acontecerá? Como você corrigiria isso se precisasse exibir a data de
lastLoginno Client Component?
- Um Server Component tenta passar um objeto
4. Resumo e Próximos Passos
Nesta aula, exploramos os padrões essenciais de comunicação entre Server e Client Components no Next.js 15:
- Server Components renderizam Client Components: Passando dados serializáveis como props. Este é o fluxo mais comum para construir a UI.
- Client Components chamam Server Actions: Para interagir com o servidor, mutar dados, etc. Server Actions são a ponte segura e performática para o lado do servidor.
- Props serializáveis: Entender o que pode e o que não pode ser passado como prop é crucial para evitar erros.
- Server Actions como props: Um padrão poderoso para delegar a execução de lógica de servidor do SC para o CC, mantendo a responsabilidade da ação no servidor.
Dominar esses padrões é fundamental para construir aplicações robustas e eficientes com o App Router do Next.js. O modelo mental deve ser sempre "server-first", buscando renderizar o máximo possível no servidor e delegando ao cliente apenas o que exige interatividade.
Próximos Passos: No próximo módulo, vamos aprofundar em "Data Fetching no Next.js 15", onde veremos como buscar dados de forma eficiente tanto em Server Components quanto em Client Components, e como os padrões de comunicação que aprendemos hoje se encaixam nesse contexto.
Até a próxima aula! 👋