iTranslated by AI
TanStack DB: A New Approach to State Management
Introduction
I looked into the new TanStack DB recently released by TanStack and will introduce an overview here.
What is TanStack DB?
TanStack DB establishes a persistence layer (DB) on the frontend and provides a mechanism to directly query from components.
Conventionally, the backend database was considered the source of truth, and the frontend would periodically query it.
TanStack DB reverses this, giving the frontend the initiative over data.
In other words, it's appealing because you can use it as if you "brought the DB to the frontend."
What are the benefits of TanStack DB?
Automatic Optimistic Updates
TanStack Query, from the same TanStack series, does not perform optimistic UI updates by default.
This means it assumes failure; the UI is only updated upon success.
To support optimistic updates, you had to manually implement temporary updates, rollbacks, and re-fetches.
const mutation = useMutation({
mutationFn: updateTodo,
// Implementing optimistic updates
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
return { previousTodos };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
Writing such processing every time is tedious and prone to errors.
With TanStack DB, simply updating a local Collection directly triggers automatic optimistic updates, eliminating the need for the complex code above.
const updateTodo = async (id: string, newTodo: TODO) => {
await todosCollection.update(id, (draft) => {
draft = newTodo; // This alone completes the optimistic update
});
};
Optimistic updates are crucial for achieving a smooth UI.
Since operations rarely fail, immediately updating the UI assuming success improves the user experience.
Freely Swappable Persistence Layer
When dealing with storage on the frontend, there are various options like localStorage and IndexedDB.
Also, when synchronizing with the backend, SaaS services such as Supabase, Firebase, or GraphQL may be used.
Typically, wrapper libraries are used to access these from the frontend.
For example, Dexie.js for IndexedDB, Apollo Client for GraphQL, Firebase SDK for Firebase, etc.
Each of these has its own unique API, so usage differs and requires a learning cost.
TanStack DB is not tied to any specific storage technology. By swapping adapters, it can support various storage options.
- localStorage
- In-memory
- REST API
- Electric SQL
- TrailBase
In addition to the officially supported adapters, you can also implement your own custom adapters.
Adapters for Supabase, IndexedDB, DuckDB Wasm, etc., are technically feasible, so their emergence is anticipated.
Potential to Simplify Backend Implementation
Since the frontend can directly query the data it needs, it may be possible to simplify backend implementation.
With TanStack DB, the backend's responsibilities can be limited to designing the database schema, setting up Row Level Security (RLS), and implementing authentication.
Electric SQL and TrailBase, mentioned above, offer mechanisms to automatically synchronize the frontend DB with the backend DB.
Traditionally, similar functionality was possible using Supabase, GraphQL's Hasura, PostGraphile, etc.
However, these technologies each have their own unique APIs, incurring a learning cost.
Frontend logic becomes dependent on a specific SaaS, and if you later want to change either the UI library or the backend, migration becomes difficult.
With TanStack DB, you only need to swap adapters, so you don't have to depend on a specific storage technology.
No matter which backend you adopt, the frontend implementation remains the same.
Moreover, the UI library supports multiple frameworks, so even if you switch frameworks—from Vue to React, or from React to Solid—the swap is easy.
Usage
Let's explain how to use it right away.
We'll explain based on using it with React this time.
The TanStack DB packages are as follows:
npm install @tanstack/db @tanstack/react-db
Defining Collections and Querying
First, define a Collection to persist data.
import { createCollection, localStorageCollectionOptions } from "@tanstack/db";
import * as v from "valibot";
const drawCalcSchema = v.object({
id: v.string(),
name: v.string(),
gameTemplate: v.string(),
result: v.string(),
createdAt: v.date(),
updatedAt: v.date(),
});
export const drawCalcCollection = createCollection(
localStorageCollectionOptions({
storageKey: "tcg-tool-draw-calculations",
id: "draw-calculations",
getKey: (item) => item.id,
schema: drawCalcSchema
}),
);
// Alternatively
type DrawCalc = {
id: string;
name: string;
gameTemplate: string;
result: string;
createdAt: Date;
updatedAt: Date;
};
export const drawCalcCollection = createCollection(
localStorageCollectionOptions<DrawCalc>({
storageKey: "tcg-tool-draw-calculations",
id: "draw-calculations",
getKey: (item) => item.id,
}),
);
This collection is equivalent to a table in SQL.
Type definitions can be passed as type parameters, or you can pass a runtime validation schema like Zod or Valibot to infer them.
Next, we fetch data using queries.
import { useLiveQuery } from "@tanstack/react-db";
import { eq } from "@tanstack/db";
const { data } = useLiveQuery((q) =>
q
.from({ calculations: calculationsCollection })
.where(({ calculations }) => eq(calculations.id, calculationId))
.select({
id: calculations.id,
name: calculations.name,
result: calculations.result,
})
.orderBy(({ calculations }) => calculations.updatedAt, "desc"),
);
By using useLiveQuery, you can subscribe to storage in real-time, and the component will re-render in response to changes.
Conceptually, it's similar to Redux's useSelector, but the selector part can be written using SQL-like queries.
Redux selectors are flexible because they are written in JavaScript, but this also means the access method is not fixed.
By unifying this with SQL-like APIs, we can expect efficient data retrieval and reduced cognitive load.
This was a real eye-opener.
Derived Collections
You can also create "derived collections" with pre-defined queries.
// Collection for history
export const drawCalcHistoryCollection = createLiveQueryCollection((q) =>
q.from({ calculations: drawCalcCollection })
.orderBy(({ calculations }) => calculations.updatedAt, "desc"),
);
// Parameterized collection
export const createDrawCalcByGameCollection = (gameTemplate: string) =>
createLiveQueryCollection((q) =>
q.from({ calculations: drawCalcCollection })
.where(({ calculations }) => eq(calculations.gameTemplate, gameTemplate))
.orderBy(({ calculations }) => calculations.updatedAt, "desc"),
);
This allows you to bundle frequently used queries into reusable forms.
Usage in Components
const CalculationList = () => {
const { data } = useLiveQuery((q) =>
q
.from({ calculations: calculationsCollection })
.select({
id: calculations.id, // Selecting only id, so it reacts only to changes in this field
})
);
return (
<>
{data.map((item) => (
<CalculationItem key={item.id} calculationId={item.id} />
))}
</>
);
};
const CalculationItem = ({ calculationId }: { calculationId: string }) => {
const { data } = useLiveQuery((q) =>
q.from({ calculations: calculationsCollection })
// Reacts only to changes in the collection for a specific id
.where(({ calculations }) => eq(calculations.id, calculationId)),
);
const [calculation] = data
if (!calculation) return <FallbackItem />;
return (
<Card.Root>
<Card.Header>
{calculation.name}
</Card.Header>
<Card.Body>
{calculation.result}
</Card.Body>
</Card.Root>
);
}
In this way, you can write queries directly within components and subscribe to partial updates.
Bonus
Changing State Management Best Practices
So far, we've focused on data fetching and synchronization with the backend, but I believe TanStack DB could even replace local state management.
This is the area where we previously used Jotai, Zustand, and similar tools.
When trying to represent complex data structures with these tools, you often need to implement state normalization and selectors, which tends to increase cognitive load.
By defining the desired shape of data with schemas and performing operations with SQL-like queries, there is potential to significantly change state management best practices.
Additionally, the ability to synchronize this directly with the backend is a noteworthy advantage.
Expected Use Cases
Let's consider some scenarios where it would be particularly effective.
-
Applications requiring real-time updates
In cases where immediate reflection of data changes is critical, such as chat applications or collaboration tools.
For example, collaborative editing tools like Miro, Figma, or Google Sheets. -
Applications requiring offline functionality
When applications need to work smoothly even in unstable network environments.
For instance, in construction, field work, or medical settings. -
Applications requiring analysis and visualization of large data sets
When there is a need to efficiently process large amounts of data and present it clearly to users.
For example, dashboards, BI tools, or stock trading applications.
Trying It Out
I tried migrating an existing application to TanStack DB with the help of Claude Code.
- Public demo: https://tcg-tool.pages.dev/draw-calc-db
- Source code: https://github.com/bmthd/tcg-tool/pull/3/files
This application is a simple calculation app that only saves data to localStorage, but I was able to implement it using only TanStack DB without adopting any state management library.
The developer experience was smooth, and because state management and data persistence are integrated, the code became simpler.
Summary
- TanStack DB accelerates frontend-centric data design
- It has the potential to become the abstraction layer for all Server State
- It integrates state management and data persistence, and may also influence backend design
I'm very excited to see how it will evolve with future updates.
Discussion