DEV Community

Cover image for Simple mutations with TanStack Query and Next.js
Elisabeth Leonhardt
Elisabeth Leonhardt

Posted on

Simple mutations with TanStack Query and Next.js

Mutations with TanStack Query have always been a little scary - at least until I decided to give them a try. After using them a lot, there is no turning back.

So today I want to show you how you can write your first mutation and guide you through the little pitfalls that can happen along the way.

The code for this example is in this repository, which contains a collection of TanStack proof of concepts I wrote. The mutation we are talking about is here.

What are mutations for?

Mutations are designed to be used for everything that's not a GET operation, so we can use them to POST, PUT and/or DELETE data. To make it simple, I set up a mock database with json-server and a few todo-items that our beloved characters from the Rick and Morty universe will have to do during the episodes. Don't forget to execute npm run json-server to start it up.

We want the result to look something like this:

Final Rick and Morty todo list

Create a new todo

Let's start with writing the function for the actual POST first. This should be very straightforward.

async function createTodo(todo) {
  const response = await fetch("http://localhost:8000/todos", {
    method: "POST",
    headers: {
      "content-type": "application/json",
    },
    body: JSON.stringify({
      id: nanoid(),
      user: todo.user,
      task: todo.task,
      done: false,
    }),
  });

  const result = await response.json();

  if (!response.ok) {
    throw new Error(result.message);
  }
  return result;
}
Enter fullscreen mode Exit fullscreen mode

This just uses fetch to send a post request with the todo passed in as an argument and returns the result. An error is thrown if something goes wrong.

Now, we can create a form to write our todos into:

     <form onSubmit={onFormSubmit} className="grid grid-cols-1 gap-4">
        <div className="flex gap-4 items-end">
          <label htmlFor="user" className="text-xl font-bold">
            User:
          </label>
          <input
            type="text"
            className="text-black flex-1"
            name="user"
            id="user"
            onChange={changeTodo}
            value={todo.user}
          />
        </div>
        <div className="flex gap-4 items-end">
          <label htmlFor="task" className="text-xl font-bold">
            Task:
          </label>
          <input
            type="text"
            className="text-black flex-1"
            name="task"
            id="task"
            onChange={changeTodo}
            value={todo.task}
          />
        </div>
        <button type="submit" className="justify-self-end">
          create todo
        </button>
      </form>
Enter fullscreen mode Exit fullscreen mode

of course, this also requires some state and functions that handle state change:

const [todo, setTodo] = useState({ user: "", task: "" });
  function changeTodo(e) {
    const newObject = {};
    newObject[e.target.name] = e.target.value;
    setTodo({ ...todo, ...newObject });
  }
Enter fullscreen mode Exit fullscreen mode

now that this is set up, let's look at the mutation itself. The useMutation hook has only one mandatory argument, which is the function that posts to our API and which has to return a Promise. In our case, that would be:

const createTodoMutation = useMutation(createTodo)
Enter fullscreen mode Exit fullscreen mode

Now, the only thing left to do is to tell the form to actually mutate data every time a todo is submitted. You can do this for example in the onSubmit handler:

  function onFormSubmit(e) {
    e.preventDefault();
    createTodoMutation.mutate(todo);
  }
Enter fullscreen mode Exit fullscreen mode

So let's recap:

  1. The user types a todo into the field and clicks submit.
  2. In the submit handler, the default is prevented and the mutate function for the useMutation hook is called. As an argument, it takes the data you want to pass to the createTodo function.
  3. The createTodo function makes the API call and now you should see that json server indicates the POST request in his logs. Also, your new todo should be visible inside the db.json file.

List all todos

That's all nice and good, but we still can't see our todos on the page, the form only allows us to submit new ones. So let's add some quick code to visualize our todos.

We need a function to fetch from our API:

async function fetchTodos() {
  return fetch("http://localhost:8000/todos").then((res) => res.json());
}
Enter fullscreen mode Exit fullscreen mode

In our component, we can use the useQuery hook to obtain our todos:

const todos = useQuery(["todos"], fetchTodos);
Enter fullscreen mode Exit fullscreen mode

To make my code a litte cleaner, I created an extra todo component:

function Todo({ todo }) {
  return (
    <div className="bg-white text-black rounded-lg grid grid-cols-[auto_1fr_auto] items-center gap-4 py-2 px-4">
      <input
        type="checkbox"
        checked={todo.done}
        onChange={(e) => changeTodoStatus(e, todo.id)}
      />
      <p>{todo.task}</p>
      <Image
        src={todo.user}
        alt="profile picture"
        className="rounded-full"
        height={60}
        with={60}
      ></Image>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

And now we only need to iterate over our todos and show them like so:

{todos.isLoading ? <span className="loader"></span> : null}
      <div className="grid grid-cols-2 gap-4 pt-8">
{todos.isSuccess
          ? todos.data.map((todo) => <Todo todo={todo} key={todo.id}></Todo>)
          : null}
      </div>
Enter fullscreen mode Exit fullscreen mode

We are done, right?

If you tried this, you would expect the new todo to appear automatically after you created it, but this doesn't happen. That's because we never told our useQuery hook to update it's query. Since the database changed, we have to find a way for the hooks to communicate with each other: Couldn't maybe the useMutation hook tell the useQuery hook that is has to update the cache?

Yes it can and that's what we are going to do. Let's change our previous useMutation hook for the following:

const queryClient = useQueryClient();
const changeTodoMutation = useMutation(updateTodo, {
    onSuccess: () => queryClient.invalidateQueries({ queryKeys: ["todos"] }),
  });
Enter fullscreen mode Exit fullscreen mode

We are deciding to invalidate the queries that contain the queryKey "todos", which is a declarative way to ask TanStack query to update the corresponding data. We could also do something like `queryClient.refetch({queryKeys: ["todos"]}), which would be imperative but the recommended way is just to invalidate the cache and let the library handle the rest.

Conclusion

That's a very basic example I have used a lot: Posting something and immediately refetching it to keep the UI updated. Maybe you already suspect how to update a single todo? Try it yourself and then check the solution in the repository.
_

Photo by Carlos Muza on Unsplash

Top comments (0)