Head shot Matteo Scotto

Matteo Scotto

Understanding TanStack DB transactions

We'll take a comprehensive look at how TanStack DB transactions work, and unleash their power by implementing a debounced and type-safe optimistic update.

If you're new to TanStack DB, you might want to check out the official documentation.
Or if you like practical examples like me, make sure to read this fantastic post by Maxi Ferreira on An Interactive Guide to TanStack DB.

TanStack DB used to be called TanStack Optimistic, and even though I think the new name is much better at reflecting the capabilities of the library, the original name leaves no doubt about what this library excels at.

Optimistic updates make any app feel fast. From a UX standpoint, they are great. But how would you go about implementing them in your current stack?

Best case scenario, you're already using TanStack Query or a similar library to handle server-side state. If you've faced this challenge before, you've probably ended up with code looking like this:

Typescriptedit-todo-mutation.ts
const queryClient = useQueryClient();
 
useMutation({
	mutationFn: updateTodo,
	// When mutate is called:
	onMutate: async (newTodo) => {
		// Cancel any outgoing refetches
		// (so they don't overwrite our optimistic update)
		await queryClient.cancelQueries({ queryKey: ["todos", newTodo.id] });
 
		// Snapshot the previous value
		const previousTodo = queryClient.getQueryData(["todos", newTodo.id]);
 
		// Optimistically update to the new value
		queryClient.setQueryData(["todos", newTodo.id], newTodo);
 
		// Return a context with the previous and new todo
		return { previousTodo, newTodo };
	},
	// If the mutation fails, use the context we returned above
	onError: (err, newTodo, context) => {
		queryClient.setQueryData(
			["todos", context.newTodo.id],
			context.previousTodo,
		);
	},
	// Always refetch after error or success:
	onSettled: (newTodo) => {
		queryClient.invalidateQueries({ queryKey: ["todos", newTodo.id] });
	},
});

You might even have recognized this snippet from the official documentation of TanStack Query.

However, I have three main concerns with this snippet:

  1. We're manipulating the query cache with no type safety
  2. We're mixing client state with server state
  3. Our optimistic update is tied to the mutation lifecycle

Sure, newTodo is typed, but queryClient.setQueryData is not! We could pass any object to the todos query key, and we'll get 0 type errors.

Another less obvious issue: TanStack Query does not distinguish your optimistic state from your actual server state. So you're responsible for handling rollback logic, race conditions, merge conflicts etc.

TanStack Query can help you by providing helpers like cancelQueries and callbacks like onError, onSuccess and onSettled but you're still reconciling state manually, plus it's a lot of (unsafe) boilerplate.

Now, let's say you're willing to write boilerplate, and ready to endure the joys of rollbacks and poorly typed mutations. I have a new challenge for you: debounce this mutation.

This is where our callbacks tricks fall apart. Our optimistic update happens whenever we call mutate (returned by useMutation). So we can't debounce mutate directly as our state won't be updated until mutate is called by our debouncer. To trigger an optimistic update, we'll need to trigger a fetch request. This is what I meant by "our optimistic update is tied to the mutation lifecycle".
This is completely incompatible with debouncing.

We could add a localState and merge it with the server state in an effect to enable us to debounce mutation separately, but at this point you're starting to build your own TanStack DB, so why not use the real thing?

Transactions in TanStack DB

Transactions encapsulate a set of operations to be executed atomically. In other words, operations inside a transaction will either all succeed, or all fail together. This is the core database concept we're going to bring to the frontend to help us implement optimistic updates.


This post is still in draft, please feel free to reach out if you have any feedback or suggestions!