Voltar
Server Actions com TanStack Query no Next.js: Um guia completo
Aprenda como combinar Server Actions com TanStack Query para criar aplicações Next.js mais eficientes e type-safe.
Os Server Actions revolucionaram a forma como lidamos com operações do servidor no Next.js, permitindo executar funções assíncronas diretamente no servidor. Quando combinados com o TanStack Query, temos uma combinação poderosa para gerenciamento de estado e cache de dados.
O que são Server Actions?
Server Actions são funções assíncronas que são executadas no servidor. Você pode usá-las em componentes do servidor e do cliente. Elas podem ser utilizadas para submissão de formulários usando form action
ou em qualquer manipulador de eventos como onClick
, onChange
, etc., ou no hook useEffect
.
Os Server Actions tornaram as coisas mais fáceis, seja para buscar dados ou mutar dados diretamente em seus React Server Components.
React Server Components
React Server Components são uma nova forma de construir aplicações React renderizadas no servidor. Eles são similares aos componentes React, mas executam no servidor e são renderizados como HTML. Como executam no servidor, podem acessar recursos do lado do servidor como bancos de dados e arquivos. Também ajudam a minimizar o tamanho do bundle JS, já que os componentes do servidor não são enviados para o cliente.
Usando Server Actions em Server Components
Você pode usar server actions diretamente em seu componente do servidor assim:
const HomePage = () => {
// Server Action
const addUserAction = async (formData: FormData) => {
'use server';
const name = formData.get('name');
console.log(name);
};
return (
<form action={addUserAction}>
<input name="name" />
</form>
);
};
export default HomePage;
Aqui com 'use server'
estamos dizendo ao servidor que esta função deve ser executada no servidor. Mas e quanto aos componentes do cliente?
Server Actions em Client Components
Para usar server actions em componentes do cliente, temos que criar um novo arquivo e marcá-lo para executar no servidor adicionando 'use server'
no topo do arquivo:
// action.ts
'use server';
export const addUserAction = async (formData: FormData) => {
'use server';
const name = formData.get('name');
console.log(name);
};
// Agora você pode usar esta action no componente cliente assim:
// client.tsx
import { addUserAction } from './action';
const ClientPage = () => {
return (
<form action={addUserAction}>
<input name="name" />
</form>
);
};
Como usar Server Actions para buscar dados com TanStack Query
O TanStack Query é uma biblioteca poderosa para busca e cache de dados para React. É parte do TanStack, uma coleção de bibliotecas que ajudam você a construir aplicações React full-stack. Ela fornece um conjunto de hooks e utilitários para buscar e cachear dados em seus componentes React.
Agora que sabemos que buscar dados com server components é fácil, mas e quanto aos componentes do cliente? Bem, com TanStack Query podemos torná-lo mais intuitivo e fácil de usar com type safety de ponta a ponta.
Importante: É recomendado usar Server Actions para mutações de dados apenas, não para busca de dados. Estou aqui compartilhando uma forma como podemos usar server actions para buscar dados também com TanStack Query.
Mas e se pudermos usar server component para buscar dados iniciais e passar para tanstack query e usar server action para refazer a busca? Soa bem, certo?
// app/page.tsx
import Post from '@/components/post';
export type TPost = {
userId: number;
id: number;
title: string;
body: string;
};
export const getAllPost = async (): Promise<TPost[]> => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const data = await response.json();
return data;
};
const HomePage = async () => {
const post = await getAllPost();
return <Post post={post} />;
};
export default HomePage;
Aqui como você pode ver, estamos buscando os dados no server component e passando para o componente cliente. Agora podemos passá-lo para o Tanstack query como dados iniciais e usar server action para refazer a busca.
// components/post.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import { postAction } from './action/post-action';
import { TPost } from '@/app/page';
const Post = ({ post }: { post: TPost[] }) => {
const { data, isLoading, refetch } = useQuery({
queryKey: [`post`],
queryFn: async () => {
const data = await postAction();
return data;
},
initialData: post,
refetchOnMount: false,
});
return (
<main>
<h1>Post</h1>
{isLoading ? (
<p>Loading...</p>
) : (
<ul>
{data?.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
)}
<button onClick={() => refetch()}>Refresh</button>
</main>
);
};
export default Post;
Aqui estamos usando Tanstack query para buscar os dados e usando server action para refazer a busca. A coisa importante a notar aqui é que temos que manter refetchOnMount
como false
para evitar refazer a busca no mount, já que já passamos os dados iniciais que buscamos no server component.
Agora sobre nosso arquivo de server action:
// action/post-action.ts
'use server';
import { getAllPost, TPost } from '@/app/page';
export const postAction = async (): Promise<TPost[]> => {
const posts = await getAllPost();
return posts.slice(0, 5);
};
Simplesmente criamos um arquivo post-action.ts
e marcamos como arquivo do servidor e criamos uma função para buscar os dados e retorná-los. Agora podemos usar esta função em nosso componente cliente para refazer a busca. Para fins de teste, estamos retornando apenas 5 posts. E a parte mais importante é que estamos reutilizando a mesma função que usamos no server component para buscar os dados.
Obviamente, sempre é bom buscar dados no server component e passar para o componente cliente e na mutação podemos revalidar usando as funções fornecidas pelo Next.js revalidatePath
e revalidateTag
. Podemos usar a nova API cache
do React para lidar com a busca de dados para o próximo nível em React Server Components.
Mutação usando Server Action com TanStack Query
Mutamos dados em nossa aplicação em múltiplos lugares e queremos dar ao usuário uma boa experiência com validação do lado do cliente para resposta rápida e estado de loading para tarefas em andamento e erro mostrado se o servidor retornar erro. Podemos fazer tudo isso manualmente, mas é aqui que o TanStack Query brilha e gerencia tudo automaticamente.
// components/post-mutation.tsx
'use client';
import { useMutation } from '@tanstack/react-query';
import { postMutationAction } from './action/postMutation-action';
const PostMutation = () => {
const { data, mutate, isPending } = useMutation({
mutationFn: postMutationAction,
onError: (error) => {
return alert(error.message || 'Falha ao atualizar');
},
onSuccess: () => {
// revalidar dados ou mostrar toast de sucesso
},
});
return (
<div>
<h1>Post Mutation</h1>
<button onClick={() => mutate({ title: 'foo', body: 'bar', userId: 1 })}>
Post
</button>
{isPending ? <p>pending...</p> : null}
{data ? <p>{JSON.stringify(data)}</p> : null}
</div>
);
};
export default PostMutation;
Aqui como você pode ver, passamos nossa função de mutação do server action para o mutationFn
do TanStack e depois disso estamos lidando com todos os outros estados para uma resposta. E isso é totalmente type-safe de ponta a ponta. O TanStack fornece a função mutate
que podemos usar para disparar a mutação e se precisarmos passar esses dados no caso de nosso server action precisar aqui, podemos simplesmente passar aqui de forma type-safe.
// postMutation-action.ts
'use server';
type TParams = {
title: string;
body: string;
userId: number;
};
type TPost = TParams & {
id: number;
};
export const postMutationAction = async (params: TParams): Promise<TPost> => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
method: 'POST',
body: JSON.stringify(params),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
});
const data = await response.json();
return data;
};
Aqui estamos simplesmente criando um arquivo de server action e passando os dados para o servidor e retornando a resposta. E estamos usando esta função em nosso componente cliente para mutar os dados.
Como você pode ver, quão fácil é usar server action com TanStack Query para buscar e mutar dados em sua aplicação. A coisa boa aqui é que estamos obtendo benefícios de ambos os lados, onde server actions nos dão poder onde não precisamos criar rotas de API e com TanStack podemos obter todo o gerenciamento de estado e cache automaticamente.
Benefícios da Combinação
1. Type Safety Completo
// Exemplo de type safety entre Server Actions e TanStack Query
type User = {
id: number;
name: string;
email: string;
};
// Server Action com tipos
const createUserAction = async (userData: Omit<User, 'id'>): Promise<User> => {
'use server';
// Lógica de criação
return { id: 1, ...userData };
};
// TanStack Query com os mesmos tipos
const { mutate } = useMutation({
mutationFn: createUserAction, // TypeScript infere os tipos automaticamente
});
2. Cache Inteligente
const { data, refetch } = useQuery({
queryKey: ['users'],
queryFn: fetchUsersAction,
initialData: serverUsers, // Dados do servidor como cache inicial
staleTime: 5 * 60 * 1000, // 5 minutos
});
3. Otimização de Performance
// Server Component busca dados iniciais
const HomePage = async () => {
const initialData = await fetchInitialData();
return (
<ClientComponent
initialData={initialData}
// TanStack Query usa os dados iniciais como cache
/>
);
};
Casos de Uso Práticos
Formulários com Validação
// action.ts
'use server';
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
export const createUserAction = async (formData: FormData) => {
const data = Object.fromEntries(formData);
const validatedData = UserSchema.parse(data);
// Criar usuário no banco
return { success: true, user: validatedData };
};
// Component.tsx
'use client';
import { useMutation } from '@tanstack/react-query';
const UserForm = () => {
const { mutate, isPending } = useMutation({
mutationFn: createUserAction,
onSuccess: () => {
// Revalidar cache ou mostrar sucesso
},
});
return (
<form action={mutate}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Criando...' : 'Criar Usuário'}
</button>
</form>
);
};
Lista com Paginação
// Server Component
const UsersPage = async ({ searchParams }: { searchParams: { page?: string } }) => {
const page = parseInt(searchParams.page || '1');
const initialUsers = await fetchUsers(page);
return <UsersList initialData={initialUsers} currentPage={page} />;
};
// Client Component
const UsersList = ({ initialData, currentPage }: Props) => {
const { data, isLoading } = useQuery({
queryKey: ['users', currentPage],
queryFn: () => fetchUsersAction(currentPage),
initialData,
});
return (
<div>
{data?.map(user => <UserCard key={user.id} user={user} />)}
<Pagination currentPage={currentPage} />
</div>
);
};
Boas Práticas
1. Separação de Responsabilidades
// actions/data-actions.ts
'use server';
export const fetchUsersAction = async (page: number) => {
// Lógica de busca
};
export const createUserAction = async (userData: UserData) => {
// Lógica de criação
};
// hooks/useUsers.ts
export const useUsers = (page: number, initialData?: User[]) => {
return useQuery({
queryKey: ['users', page],
queryFn: () => fetchUsersAction(page),
initialData,
});
};
2. Tratamento de Erros
const { mutate, error } = useMutation({
mutationFn: createUserAction,
onError: (error) => {
if (error instanceof Error) {
toast.error(error.message);
}
},
});
3. Otimização de Cache
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: createUserAction,
onSuccess: (newUser) => {
// Atualizar cache otimisticamente
queryClient.setQueryData(['users'], (old: User[] = []) => [
...old,
newUser
]);
},
});
Conclusão
React Server Components é um novo paradigma que nos ajuda a renderizar algumas partes de nossa aplicação diretamente no servidor e enviar o HTML para o cliente, após isso o React hidrata e torna interativo. Server Actions levaram isso para o próximo nível onde podemos usar server actions para buscar dados e mutar dados diretamente em nossos server components.
TanStack Query e Server Actions são duas ferramentas poderosas que podem ser usadas juntas para tornar nossa aplicação mais intuitiva e fácil de usar. Com TanStack Query podemos buscar e cachear dados em nossa aplicação e com Server Actions podemos mutar dados e buscar dados em nossos server components.
Dica: Comece usando Server Actions para mutações simples e gradualmente adicione TanStack Query para gerenciamento de cache mais complexo. A combinação oferece o melhor dos dois mundos: performance do servidor e experiência rica do cliente.
Espero que este guia tenha ajudado você a entender como usar Server Actions com TanStack Query no Next.js. Até a próxima! 🚀