Fundamentos do Next.js 15

0/26 aulas0%
pratica

Criando e Gerenciando Dados (CRUD) com Server Actions

Aprenda sobre criando e gerenciando dados (crud) com server actions

60 min
Aula 4 de 6

Criando e Gerenciando Dados (CRUD) com Server Actions

Bem-vindos à aula prática do Módulo 5! 👋 Nesta etapa crucial do nosso projeto final, vamos mergulhar no coração da interatividade de uma aplicação full stack: a criação, leitura, atualização e exclusão (CRUD) de dados. E a ferramenta que usaremos para fazer isso de forma elegante e eficiente no Next.js 15 são os Server Actions.

🚀 1. Introdução: O Poder dos Server Actions

Até agora, você provavelmente já está familiarizado com a leitura de dados utilizando fetch em Server Components. Mas e quando precisamos modificar dados? Inserir um novo item, atualizar um registro existente ou deletar algo? Tradicionalmente, isso envolveria a criação de rotas de API separadas (/api/minha-acao), o que adiciona uma camada de complexidade e boilerplate.

É aí que os Server Actions brilham! ✨

Server Actions são funções assíncronas que executam diretamente no servidor. Eles permitem que você defina mutações de dados e outras operações de backend dentro dos seus componentes React (ou em arquivos separados), sem a necessidade de criar rotas de API REST/GraphQL explícitas. Isso simplifica drasticamente o desenvolvimento de aplicações full stack, aproximando o código do frontend e do backend de uma maneira segura e performática.

Por que Server Actions?

  • Simplificação: Reduz a necessidade de rotas de API, diminuindo o boilerplate.
  • Performance: Menos requisições de rede, pois a função pode ser chamada diretamente.
  • Segurança: Executam no servidor, protegendo segredos e lógica de negócios.
  • Integração: Funcionam perfeitamente com formulários HTML e hooks como useTransition e useOptimistic do React.

Nesta aula, vamos construir funcionalidades CRUD usando Server Actions para interagir com um "banco de dados" simulado (ou, para simplificar, um array em memória que simula um armazenamento persistente para fins didáticos, ou você pode conectar a um banco de dados real se já tiver configurado).

💡 2. Explicação Detalhada com Exemplos

Vamos entender como Server Actions funcionam na prática.

2.1. Definindo um Server Action

Um Server Action é simplesmente uma função assíncrona marcada com a diretiva 'use server'. Essa diretiva informa ao compilador do Next.js que esta função deve ser executada exclusivamente no servidor.

Você pode definir um Server Action de duas maneiras principais:

  1. Dentro de um Server Component:
    // app/dashboard/page.tsx
    import { revalidatePath } from 'next/cache';
     
    export default function DashboardPage() {
      async function createPost(formData: FormData) {
        'use server'; // <--- A diretiva 'use server'
        const title = formData.get('title') as string;
        const content = formData.get('content') as string;
     
        // Lógica para salvar no banco de dados
        console.log('Novo post criado:', { title, content });
        // Simulando um delay de rede
        await new Promise(resolve => setTimeout(resolve, 1000));
     
        // Revalidar o cache para que a lista de posts seja atualizada
        revalidatePath('/dashboard');
      }
     
      return (
        <div>
          <h1>Criar Novo Post</h1>
          <form action={createPost}>
            <input type="text" name="title" placeholder="Título do Post" required />
            <textarea name="content" placeholder="Conteúdo do Post" required />
            <button type="submit">Criar Post</button>
          </form>
        </div>
      );
    }
  2. Em um arquivo separado (recomendado para organização):
    // app/actions.ts
    'use server'; // <--- A diretiva 'use server' no topo do arquivo aplica-se a todas as exportações
     
    import { revalidatePath } from 'next/cache';
     
    interface Post {
      id: string;
      title: string;
      content: string;
    }
     
    // Simulando um banco de dados em memória
    const posts: Post[] = [
      { id: '1', title: 'Meu Primeiro Post', content: 'Conteúdo inicial.' },
    ];
    let nextId = 2;
     
    export async function createPost(formData: FormData) {
      const title = formData.get('title') as string;
      const content = formData.get('content') as string;
     
      if (!title || !content) {
        throw new Error('Título e conteúdo são obrigatórios.');
      }
     
      const newPost: Post = { id: String(nextId++), title, content };
      posts.push(newPost);
     
      console.log('Novo post criado:', newPost);
      await new Promise(resolve => setTimeout(resolve, 500)); // Simula delay de rede
     
      revalidatePath('/dashboard'); // Revalida a página para mostrar o novo post
      return newPost; // Opcional: retornar dados
    }
     
    export async function getPosts(): Promise<Post[]> {
      // Esta função não é um Server Action para mutação, mas pode ser usada para buscar dados
      // Se for chamada de um Client Component, ela se tornará uma API call.
      // Se for chamada de um Server Component, ela é apenas uma função do servidor.
      await new Promise(resolve => setTimeout(resolve, 300));
      return posts;
    }
     
    export async function deletePost(id: string) {
      const initialLength = posts.length;
      const index = posts.findIndex(post => post.id === id);
      if (index > -1) {
        posts.splice(index, 1);
      }
     
      console.log(`Post ${id} deletado.`);
      await new Promise(resolve => setTimeout(resolve, 500));
      revalidatePath('/dashboard');
      return initialLength !== posts.length; // Retorna true se deletou
    }
     
    export async function updatePost(id: string, formData: FormData) {
      const title = formData.get('title') as string;
      const content = formData.get('content') as string;
     
      if (!title || !content) {
        throw new Error('Título e conteúdo são obrigatórios.');
      }
     
      const postToUpdate = posts.find(post => post.id === id);
      if (postToUpdate) {
        postToUpdate.title = title;
        postToUpdate.content = content;
      }
     
      console.log(`Post ${id} atualizado.`);
      await new Promise(resolve => setTimeout(resolve, 500));
      revalidatePath('/dashboard');
      return postToUpdate;
    }

2.2. Invocando Server Actions

A maneira mais comum e poderosa de invocar um Server Action é através do atributo action de um elemento <form>. Quando o formulário é submetido, o Next.js intercepta a submissão e chama o Server Action.

// app/dashboard/page.tsx
import { createPost, getPosts, deletePost } from '@/app/actions'; // Importe os Server Actions
import PostCard from '@/components/PostCard'; // Componente de exemplo para exibir post
 
export default async function DashboardPage() {
  const posts = await getPosts(); // Assume que getPosts é um Server Function
 
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">Gerenciar Posts</h1>
 
      <section className="mb-8 p-6 bg-gray-100 rounded-lg shadow-md">
        <h2 className="text-2xl font-semibold mb-4">Criar Novo Post</h2>
        <form action={createPost} className="space-y-4">
          <div>
            <label htmlFor="title" className="block text-sm font-medium text-gray-700">Título:</label>
            <input
              type="text"
              id="title"
              name="title"
              placeholder="Título do Post"
              required
              className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
          <div>
            <label htmlFor="content" className="block text-sm font-medium text-gray-700">Conteúdo:</label>
            <textarea
              id="content"
              name="content"
              placeholder="Conteúdo do Post"
              required
              rows={4}
              className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
            ></textarea>
          </div>
          <button
            type="submit"
            className="px-4 py-2 bg-blue-600 text-white font-semibold rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
          >
            Criar Post
          </button>
        </form>
      </section>
 
      <section>
        <h2 className="text-2xl font-semibold mb-4">Posts Existentes</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {posts.map((post) => (
            <PostCard key={post.id} post={post} />
          ))}
        </div>
      </section>
    </div>
  );
}
 
// components/PostCard.tsx (Exemplo de Client Component para exibir e deletar)
'use client';
 
import { deletePost } from '@/app/actions';
import { useTransition } from 'react';
 
interface PostCardProps {
  post: {
    id: string;
    title: string;
    content: string;
  };
}
 
export default function PostCard({ post }: PostCardProps) {
  const [isPending, startTransition] = useTransition();
 
  const handleDelete = async () => {
    startTransition(async () => {
      await deletePost(post.id);
    });
  };
 
  return (
    <div className="bg-white p-4 rounded-lg shadow-md border border-gray-200">
      <h3 className="text-xl font-bold mb-2">{post.title}</h3>
      <p className="text-gray-700 mb-4">{post.content}</p>
      <button
        onClick={handleDelete}
        disabled={isPending}
        className="px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed"
      >
        {isPending ? 'Deletando...' : 'Deletar'}
      </button>
      {/* Aqui você poderia adicionar um botão de Editar que abre um modal ou navega para uma página de edição */}
    </div>
  );
}

Observações:

  • FormData: Quando um Server Action é invocado via form action, o primeiro argumento que ele recebe é um objeto FormData, que contém todos os campos do formulário.
  • revalidatePath: Após uma mutação, é crucial chamar revalidatePath('/seu-caminho') ou revalidateTag('sua-tag') para informar ao Next.js que os dados em cache para aquela rota/tag estão desatualizados e precisam ser buscados novamente na próxima renderização. Isso garante que o usuário veja as alterações.
  • useTransition (para Client Components): Se você precisar chamar um Server Action de um Client Component (por exemplo, um botão de "Deletar" que não está dentro de um <form>), você pode usar o hook useTransition do React. Ele permite que você mantenha a UI responsiva enquanto a ação assíncrona está em andamento, sem bloquear a interface. isPending indica se a transição está ativa.

2.3. Manipulando Erros

É vital lidar com erros que podem ocorrer durante a execução de um Server Action. Use blocos try...catch dentro do seu Server Action.

// app/actions.ts (atualizado)
'use server';
 
import { revalidatePath } from 'next/cache';
 
// ... (definição de Post, posts, nextId)
 
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
 
  try {
    if (!title || !content) {
      throw new Error('Título e conteúdo são obrigatórios.');
    }
 
    const newPost = { id: String(nextId++), title, content };
    posts.push(newPost);
 
    console.log('Novo post criado:', newPost);
    await new Promise(resolve => setTimeout(resolve, 500));
 
    revalidatePath('/dashboard');
    return { success: true, data: newPost }; // Retorna um objeto com status
  } catch (error: any) {
    console.error('Erro ao criar post:', error.message);
    return { success: false, error: error.message }; // Retorna o erro
  }
}

No Client Component, você pode verificar o resultado:

// components/PostForm.tsx (Exemplo de Client Component para formulário com feedback)
'use client';
 
import { createPost } from '@/app/actions';
import { useRef, useState } from 'react';
 
export default function PostForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const [message, setMessage] = useState('');
  const [isError, setIsError] = useState(false);
 
  const handleSubmit = async (formData: FormData) => {
    setMessage('');
    setIsError(false);
    const result = await createPost(formData);
 
    if (result.success) {
      setMessage('Post criado com sucesso!');
      formRef.current?.reset(); // Limpa o formulário
    } else {
      setIsError(true);
      setMessage(`Erro: ${result.error}`);
    }
  };
 
  return (
    <form ref={formRef} action={handleSubmit} className="space-y-4">
      {/* ... campos do formulário ... */}
      <button
        type="submit"
        className="px-4 py-2 bg-blue-600 text-white font-semibold rounded-md hover:bg-blue-700"
      >
        Criar Post
      </button>
      {message && (
        <p className={`mt-2 text-sm ${isError ? 'text-red-600' : 'text-green-600'}`}>
          {message}
        </p>
      )}
    </form>
  );
}

2.4. Validação de Entrada

Sempre valide os dados de entrada no servidor. Isso é crucial para a segurança e integridade dos seus dados. Você pode usar bibliotecas como zod para isso.

// app/actions.ts (com validação Zod)
'use server';
 
import { revalidatePath } from 'next/cache';
import { z } from 'zod'; // Certifique-se de ter 'zod' instalado: npm install zod
 
// ... (definição de Post, posts, nextId)
 
const postSchema = z.object({
  title: z.string().min(3, { message: 'O título deve ter pelo menos 3 caracteres.' }).max(100),
  content: z.string().min(10, { message: 'O conteúdo deve ter pelo menos 10 caracteres.' }),
});
 
export async function createPost(formData: FormData) {
  const rawData = {
    title: formData.get('title'),
    content: formData.get('content'),
  };
 
  try {
    const validatedData = postSchema.parse(rawData); // Valida os dados
 
    const newPost = { id: String(nextId++), ...validatedData };
    posts.push(newPost);
 
    console.log('Novo post criado:', newPost);
    await new Promise(resolve => setTimeout(resolve, 500));
 
    revalidatePath('/dashboard');
    return { success: true, data: newPost };
  } catch (error: any) {
    if (error instanceof z.ZodError) {
      // Erro de validação do Zod
      const issues = error.issues.map(issue => issue.message).join(', ');
      return { success: false, error: `Erro de validação: ${issues}` };
    }
    console.error('Erro ao criar post:', error.message);
    return { success: false, error: error.message || 'Ocorreu um erro desconhecido.' };
  }
}

🛠️ 3. Exercícios Práticos: Construindo um Gerenciador de Tarefas

Vamos aplicar o que aprendemos construindo um pequeno gerenciador de tarefas (Todo List) que utiliza Server Actions para todas as operações CRUD.

Pré-requisitos:

  • Um projeto Next.js 15 configurado (com App Router).
  • npm install zod para validação.
  • Opcional: npm install tailwindcss postcss autoprefixer e configure o Tailwind CSS para estilização rápida.

Tarefas:

  1. Estrutura Inicial:

    • Crie um arquivo app/actions.ts para seus Server Actions.
    • Crie uma página app/todos/page.tsx para exibir e gerenciar as tarefas.
    • (Opcional) Crie um componente components/TodoItem.tsx para cada item da lista.
  2. Definir o "Banco de Dados" (em memória):

    • No app/actions.ts, crie um array global todos e um nextId para simular um armazenamento.
    • Defina uma interface Todo (ex: id: string; text: string; completed: boolean;).
    // app/actions.ts
    'use server';
     
    import { revalidatePath } from 'next/cache';
    import { z } from 'zod';
     
    interface Todo {
      id: string;
      text: string;
      completed: boolean;
    }
     
    const todos: Todo[] = [
      { id: '1', text: 'Aprender Server Actions', completed: false },
      { id: '2', text: 'Construir um projeto Next.js', completed: true },
    ];
    let nextId = 3;
     
    const todoSchema = z.object({
      text: z.string().min(1, { message: 'A tarefa não pode ser vazia.' }).max(200),
    });
     
    // ... (Seus Server Actions virão aqui)
  3. READ (Leitura de Tarefas):

    • Crie uma função getTodos() em app/actions.ts que retorna o array todos.
    • Em app/todos/page.tsx, chame getTodos() dentro do Server Component para exibir a lista de tarefas.
    // app/actions.ts
    // ...
    export async function getTodos(): Promise<Todo[]> {
      await new Promise(resolve => setTimeout(resolve, 300)); // Simula delay
      return todos;
    }
    // app/todos/page.tsx
    import { getTodos } from '@/app/actions';
    import AddTodoForm from '@/components/AddTodoForm'; // Criaremos este
    import TodoItem from '@/components/TodoItem'; // Criaremos este
     
    export default async function TodosPage() {
      const todos = await getTodos();
     
      return (
        <div className="container mx-auto p-4 max-w-xl">
          <h1 className="text-4xl font-bold text-center mb-8">Meu Gerenciador de Tarefas</h1>
     
          <AddTodoForm /> {/* Formulário para adicionar */}
     
          <section className="mt-8">
            <h2 className="text-2xl font-semibold mb-4">Minhas Tarefas</h2>
            {todos.length === 0 ? (
              <p className="text-gray-600 text-center">Nenhuma tarefa ainda. Adicione uma!</p>
            ) : (
              <ul className="space-y-4">
                {todos.map((todo) => (
                  <TodoItem key={todo.id} todo={todo} />
                ))}
              </ul>
            )}
          </section>
        </div>
      );
    }
  4. CREATE (Criação de Tarefas):

    • Crie um Server Action addTodo(formData: FormData) em app/actions.ts.
      • Use todoSchema.parse para validar o text.
      • Adicione a nova tarefa ao array todos.
      • Chame revalidatePath('/todos').
      • Retorne um objeto { success: true, data: newTodo } ou { success: false, error: message }.
    • Crie um Client Component components/AddTodoForm.tsx.
      • Ele deve conter um <form> com um <input type="text" name="text"> e um <button type="submit">.
      • Use o atributo action do formulário para chamar o addTodo.
      • Implemente feedback visual (mensagem de sucesso/erro) e limpe o formulário após o sucesso.
    // app/actions.ts (adicionar)
    // ...
    export async function addTodo(formData: FormData) {
      const rawText = formData.get('text');
      try {
        const { text } = todoSchema.parse({ text: rawText });
     
        const newTodo: Todo = { id: String(nextId++), text, completed: false };
        todos.push(newTodo);
     
        await new Promise(resolve => setTimeout(resolve, 500));
        revalidatePath('/todos');
        return { success: true, data: newTodo };
      } catch (error: any) {
        if (error instanceof z.ZodError) {
          return { success: false, error: error.issues[0].message };
        }
        return { success: false, error: 'Falha ao adicionar tarefa.' };
      }
    }
    // components/AddTodoForm.tsx
    'use client';
     
    import { addTodo } from '@/app/actions';
    import { useRef, useState } from 'react';
     
    export default function AddTodoForm() {
      const formRef = useRef<HTMLFormElement>(null);
      const [message, setMessage] = useState('');
      const [isError, setIsError] = useState(false);
     
      const handleSubmit = async (formData: FormData) => {
        setMessage('');
        setIsError(false);
        const result = await addTodo(formData);
     
        if (result.success) {
          setMessage('Tarefa adicionada com sucesso!');
          formRef.current?.reset();
        } else {
          setIsError(true);
          setMessage(`Erro: ${result.error}`);
        }
      };
     
      return (
        <div className="p-6 bg-white rounded-lg shadow-md mb-8">
          <h2 className="text-2xl font-semibold mb-4">Adicionar Nova Tarefa</h2>
          <form ref={formRef} action={handleSubmit} className="flex flex-col gap-4">
            <input
              type="text"
              name="text"
              placeholder="O que precisa ser feito?"
              required
              className="flex-grow p-3 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
            />
            <button
              type="submit"
              className="px-5 py-3 bg-blue-600 text-white font-semibold rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
            >
              Adicionar Tarefa
            </button>
            {message && (
              <p className={`mt-2 text-sm ${isError ? 'text-red-600' : 'text-green-600'}`}>
                {message}
              </p>
            )}
          </form>
        </div>
      );
    }
  5. UPDATE (Atualização de Tarefas - Marcar como Concluída):

    • Crie um Server Action toggleTodo(id: string) em app/actions.ts.
      • Encontre a tarefa pelo id e inverta seu status completed.
      • Chame revalidatePath('/todos').
    • Crie um Client Component components/TodoItem.tsx.
      • Ele deve exibir o texto da tarefa e um checkbox.
      • Use useTransition e um onClick no checkbox para chamar toggleTodo.
      • Exiba o texto da tarefa riscado se completed for true.
    // app/actions.ts (adicionar)
    // ...
    export async function toggleTodo(id: string) {
      try {
        const todo = todos.find(t => t.id === id);
        if (!todo) {
          throw new Error('Tarefa não encontrada.');
        }
        todo.completed = !todo.completed;
     
        await new Promise(resolve => setTimeout(resolve, 300));
        revalidatePath('/todos');
        return { success: true };
      } catch (error: any) {
        console.error('Erro ao alternar tarefa:', error.message);
        return { success: false, error: error.message };
      }
    }
    // components/TodoItem.tsx
    'use client';
     
    import { toggleTodo } from '@/app/actions';
    import { useTransition } from 'react';
     
    interface TodoItemProps {
      todo: {
        id: string;
        text: string;
        completed: boolean;
      };
    }
     
    export default function TodoItem({ todo }: TodoItemProps) {
      const [isPending, startTransition] = useTransition();
     
      const handleToggle = async () => {
        startTransition(async () => {
          await toggleTodo(todo.id);
        });
      };
     
      return (
        <li className="flex items-center bg-gray-50 p-4 rounded-md shadow-sm border border-gray-200">
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={handleToggle}
            disabled={isPending}
            className="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
          />
          <span
            className={`ml-3 text-lg flex-grow ${
              todo.completed ? 'line-through text-gray-500' : 'text-gray-900'
            }`}
          >
            {todo.text}
          </span>
          {/* Adicione o botão de deletar aqui no próximo passo */}
        </li>
      );
    }
  6. DELETE (Exclusão de Tarefas):

    • Crie um Server Action deleteTodo(id: string) em app/actions.ts.
      • Remova a tarefa do array todos pelo id.
      • Chame revalidatePath('/todos').
    • Em components/TodoItem.tsx, adicione um botão "Deletar".
      • Use useTransition e um onClick para chamar deleteTodo.
    // app/actions.ts (adicionar)
    // ...
    export async function deleteTodo(id: string) {
      try {
        const initialLength = todos.length;
        const index = todos.findIndex(t => t.id === id);
        if (index > -1) {
          todos.splice(index, 1);
        } else {
          throw new Error('Tarefa não encontrada para exclusão.');
        }
     
        await new Promise(resolve => setTimeout(resolve, 300));
        revalidatePath('/todos');
        return { success: initialLength !== todos.length };
      } catch (error: any) {
        console.error('Erro ao deletar tarefa:', error.message);
        return { success: false, error: error.message };
      }
    }
    // components/TodoItem.tsx (atualizar)
    'use client';
     
    import { toggleTodo, deleteTodo } from '@/app/actions'; // Importe deleteTodo
    import { useTransition } from 'react';
     
    // ... (interface TodoItemProps)
     
    export default function TodoItem({ todo }: TodoItemProps) {
      const [isPendingToggle, startToggleTransition] = useTransition();
      const [isPendingDelete, startDeleteTransition] = useTransition();
     
      const handleToggle = async () => {
        startToggleTransition(async () => {
          await toggleTodo(todo.id);
        });
      };
     
      const handleDelete = async () => {
        if (confirm('Tem certeza que deseja deletar esta tarefa?')) {
          startDeleteTransition(async () => {
            await deleteTodo(todo.id);
          });
        }
      };
     
      return (
        <li className="flex items-center bg-gray-50 p-4 rounded-md shadow-sm border border-gray-200">
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={handleToggle}
            disabled={isPendingToggle}
            className="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
          />
          <span
            className={`ml-3 text-lg flex-grow ${
              todo.completed ? 'line-through text-gray-500' : 'text-gray-900'
            }`}
          >
            {todo.text}
          </span>
          <button
            onClick={handleDelete}
            disabled={isPendingDelete}
            className="ml-4 px-3 py-1 bg-red-500 text-white rounded-md hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
          >
            {isPendingDelete ? 'Deletando...' : 'Deletar'}
          </button>
        </li>
      );
    }

Após completar esses passos, você terá uma aplicação Next.js full stack funcional, utilizando Server Actions para gerenciar seus dados de forma eficiente! 🎉

📝 4. Resumo e Próximos Passos

Parabéns! Você dominou a arte de criar e gerenciar dados usando Server Actions no Next.js 15.

Pontos Chave Desta Aula:

  • Server Actions permitem que você execute código diretamente no servidor, eliminando a necessidade de rotas de API para mutações.
  • A diretiva 'use server' é essencial para definir um Server Action.
  • Formulários HTML com o atributo action são a maneira mais comum de invocar Server Actions, passando automaticamente o FormData.
  • revalidatePath (ou revalidateTag) é crucial para atualizar o cache e garantir que as mudanças de dados sejam refletidas na UI.
  • useTransition permite que você mantenha a UI responsiva ao chamar Server Actions de Client Components.
  • Validação de entrada (com Zod, por exemplo) e tratamento de erros são fundamentais para a robustez da sua aplicação.

Próximos Passos:

  • Otimização de UI: Explore o hook useOptimistic do React para criar experiências de usuário ainda mais fluidas, onde as mudanças são refletidas instantaneamente na UI antes mesmo da resposta do servidor.
  • Conectando a um Banco de Dados Real: Substitua o array em memória por uma conexão a um banco de dados real (PostgreSQL, MongoDB, etc.) usando um ORM como Prisma ou Drizzle.
  • Autenticação e Autorização: Integre Server Actions com soluções de autenticação para proteger suas mutações de dados, garantindo que apenas usuários autorizados possam realizar certas operações.
  • Integração com Caching Avançado: Aprofunde-se no sistema de cache do Next.js para otimizar ainda mais o desempenho da sua aplicação.

Continue praticando e explorando as possibilidades dos Server Actions. Eles são uma peça fundamental para construir aplicações full stack modernas e eficientes com Next.js!

© 2025 Escola All Dev. Todos os direitos reservados.

Criando e Gerenciando Dados (CRUD) com Server Actions - Fundamentos do Next.js 15 | escola.all.dev.br