This article is published in English.
Sending Data with RTK Query: A Practical Guide to Mutations
Learn how to use builder.mutation() in RTK Query to send POST requests, manage loading and error states, and build a working form component.
Introduction
Earlier we covered how to set up Redux Toolkit Query (RTK Query) and run read operations with builder.query(). That let us pull data from an API and show it inside a React app without hand-rolling useEffect(), useState(), or custom fetch logic.
Reading data, though, is only half the story when working with APIs. Most applications also need ways to add, change, or remove records on the server.
Consider a few common scenarios:
- A sign-up form submits new account details.
- A login screen sends credentials to be verified.
- A blogging platform publishes new articles.
- An online store places new orders.
- A to-do app saves newly added tasks.
Each of these actions pushes information from the client to the server, and that's normally done through an HTTP POST request.
RTK Query doesn't treat POST calls as Queries — it treats them as Mutations.
This article walks through sending data with builder.mutation(). We'll break down each piece of code and every configuration setting so you understand not just what to type, but why each part matters.
Understanding HTTP Methods
Before diving into code, it helps to review the different HTTP verbs and what they're meant for.
A typical REST API exposes several operations:
| Method | Purpose | Example |
|---|---|---|
| GET | Read data | Fetch all users |
| POST | Add new data | Create a user |
| PUT | Overwrite an existing resource | Replace a user record |
| PATCH | Modify part of a resource | Change a user's name |
| DELETE | Remove data | Delete a user |
This guide zeroes in on POST, the method used for creating new resources on a server.
Why Doesn’t POST Use builder.query()?
This is a common point of confusion for newcomers.
If builder.query() can retrieve data, why can't the same function push data to the server?
The reason comes down to what each tool is built for.
Queries
Queries exist for fetching information.
Typical examples:
- Get Users
- Get Products
- Get Orders
- Get Posts
Because the same data might be requested repeatedly, queries automatically cache their results.
Mutations
Mutations exist for modifying data.
Typical examples:
- Create User
- Update User
- Delete User
- Login
- Register
A mutation signals to the server that something needs to change.
That distinction is why RTK Query handles mutations separately from queries.
What We Are Going to Build
We'll build a basic form that submits a new user to a server.
The target endpoint is:
https://jsonplaceholder.typicode.com/users
And the payload sent in the request body will look like this:
{
"name": "John Doe",
"email": "john@example.com"
}
Project Structure
src
│
├── app
│ └── store.js
│
├── services
│ └── api.js
│
├── components
│ └── AddUser.jsx
│
├── App.jsx
│
└── main.jsx
The Redux store configuration stays as it was before. All we need to add is a mutation endpoint plus a component that handles form submission.
Step 1 — Create a Mutation Endpoint
Open the API service file:
src/services/api.js
Then add the new endpoint definition inside the endpoints object.
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const api = createApi({
reducerPath: "api", baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com/",
}), endpoints: (builder) => ({ addUser: builder.mutation({ query: (newUser) => ({
url: "users",
method: "POST",
body: newUser,
}), }), }),});export const {
useAddUserMutation,
} = api;
Let's go through this line by line.
Understanding builder.mutation()
addUser: builder.mutation({
While builder.query() is meant for fetching data, builder.mutation() is what you reach for whenever you need to alter something on the server.
Common scenarios where it applies:
- Creating users
- Registering accounts
- Logging in
- Updating products
- Deleting posts
Any time your app writes or changes data on the backend, a mutation is the right tool.
Understanding query()
query: (newUser) => ({
This function receives whatever data you pass to it from your React code.
For example, if you dispatch:
addUser({
name: "John",
email: "john@example.com",
});
then the parameter named
newUser
will hold:
{
name: "John",
email: "john@example.com"
}
That object is what gets sent as the request body.
Understanding URL
url: "users",
Given that the base URL is configured as:
https://jsonplaceholder.typicode.com/
RTK Query combines them automatically into:
https://jsonplaceholder.typicode.com/users
so you never have to write out the full address yourself.
Understanding Method
method: "POST",
This line explicitly tells RTK Query to issue a POST request. If you leave it out, the request falls back to GET by default.
Understanding Body
body: newUser,
Whatever is stored in newUser gets forwarded as the request payload, for instance:
{
"name": "John",
"email": "john@example.com"
}
The server receives that object exactly as constructed.
Step 2 — Export the Generated Hook
export const {
useAddUserMutation,
} = api;
Just as queries give you an auto-generated hook like
useGetUsersQuery()
mutations produce their own hook automatically:
useAddUserMutation()
You never write this hook by hand — RTK Query builds it for you based on the endpoint name.
Step 3 — Create the React Component
Create a new file:
src/components/AddUser.jsx
and add this code:
import { useState } from "react";
import { useAddUserMutation } from "../services/api";const AddUser = () => { const [name, setName] = useState("");
const [email, setEmail] = useState(""); const [
addUser,
{
isLoading,
isSuccess,
error,
},
] = useAddUserMutation(); const handleSubmit = async (e) => { e.preventDefault(); await addUser({
name,
email,
}); setName("");
setEmail(""); }; return (
<form onSubmit={handleSubmit}> <input
type="text"
placeholder="Enter Name"
value={name}
onChange={(e) => setName(e.target.value)}
/> <input
type="email"
placeholder="Enter Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/> <button type="submit">
Add User
</button> {isLoading && <p>Saving...</p>} {isSuccess && <p>User Added Successfully.</p>} {error && <p>Something went wrong.</p>} </form>
);};export default AddUser;
Let's break down what's happening here.
Understanding useAddUserMutation()
const [
addUser,
{
isLoading,
isSuccess,
error,
},
] = useAddUserMutation();
Unlike query hooks, mutation hooks return an array instead of an object. The first element:
addUser
is the function you call to trigger the request, while the second element is an object carrying useful status details about that request.
Understanding addUser()
await addUser({
name,
email,
});
Calling this function fires off a request like this:
POST /users
carrying a JSON payload shaped like this:
{
"name": "John",
"email": "john@example.com"
}
On the backend, that payload is used to create a brand-new user record.
Understanding Mutation States
Alongside the trigger function, RTK Query hands you a handful of status flags that describe what's happening with the request.
isLoading
isLoading
This flag flips to true while the mutation is in flight, which makes it a natural fit for disabling a submit button or showing a spinner until the response comes back.
isSuccess
isSuccess
Once the request completes without errors, this becomes true, giving you a clean signal for showing a confirmation message or navigating the user elsewhere.
error
error
If the server responds with a failure, the details land here, letting you surface a readable error instead of a broken UI.
Step 4 — Render the Component
Open the main application file:
src/App.jsx
and swap its contents for the following:
import AddUser from "./components/AddUser";
function App() {
return <AddUser />;
}export default App;
Then start the dev server:
npm run dev
Fill in the form fields and press Add User — RTK Query takes care of firing the POST request for you.
Complete Request Flow
Here's a summary of what occurs under the hood, from form submission to state update:
User Fills Form
│
▼
Clicks Submit
│
▼
addUser()
│
▼
Generated Mutation Hook
│
▼
RTK Query
│
▼
fetchBaseQuery()
│
▼
POST Request
│
▼
Server Response
│
▼
Mutation State Updates
│
▼
React Re-renders
Notice everything that's absent from this flow:
fetch()axios.post()useEffect()- Manually tracked loading state
- Manually tracked error state
RTK Query takes care of every one of these behind the scenes.
builder.query() vs builder.mutation()
Knowing when to reach for each of these builder methods matters a lot.
builder.query() is meant for fetching data, typically through GET requests, and it produces hooks such as useGetUsersQuery() that run automatically as soon as the component renders. builder.mutation(), by contrast, is meant for changing data through methods like POST, PUT, PATCH or DELETE. It produces hooks such as useAddUserMutation() that only run when you explicitly call the trigger function, rather than firing on render. In short, queries are for reading, mutations are for creating, updating or deleting.
Picking the right tool for each job keeps your API logic consistent and easy to reason about.
Best Practices
Keep these guidelines in mind whenever you build POST functionality with RTK Query:
- Reach for
builder.mutation()whenever an operation modifies data on the server. - Keep your request bodies lean, sending only the fields the backend actually needs.
- Always account for
isLoading,isSuccess, anderrorso the interface feels responsive and informative. - Choose descriptive endpoint names like
addUser,createPost, orregisterUser. - Validate whatever the user typed before sending it off to the server.
- Look into
unwrap()if you'd rather handle success and failure with atry...catchblock inside your components.
Key Takeaways
Working through this guide, you've covered how to:
- Set up a mutation with
builder.mutation(). - Wire up a POST endpoint inside an API slice.
- Ship JSON data to a backend service.
- Make use of the auto-generated
useAddUserMutation()hook. - Kick off a POST request straight from a React form.
- Manage loading, success, and error states without hand-rolled boilerplate.
- Tell queries and mutations apart.
- Apply solid practices for building maintainable API interactions.
This same pattern shows up constantly in production apps — user sign-up flows, authentication, publishing blog posts, placing orders, and countless other data-creation scenarios.
What's Next?
With POST requests under your belt, the natural next step is learning how to update and delete existing records.
The upcoming guide will cover:
- Updating records with PUT and PATCH requests.
- Removing records via DELETE requests.
- Passing dynamic IDs into mutation endpoints.
- Invalidating cached data so the UI refreshes automatically.
- Using tags —
providesTagsandinvalidatesTags— to keep everything in sync without manual refetching.
By the time you finish that guide, you'll be equipped to build a full CRUD application using production-ready RTK Query patterns.