Tanstack Query is one of the most popular solutions for handling API requests, pending and error states, and more. It provides a lot of useful functionality, such as background refetching, query deduplication and invalidation, or handling paginated and infinite queries. What's more, it offers adapters for multiple frameworks, so it can be used with React, Vue and a few other frameworks. However, did you know that Tanstack Query isn't just for API requests? Technically, it is an async state manager. In this article, we will cover a few interesting use cases for using Tanstack Query that do not involve API requests:
- Browser APIs
- IndexedDB
- Async Libraries
- Web Workers
Tanstack Query and Browser APIs
Browsers offer a lot of built-in Web APIs. Some of them are synchronous, such as local storage, or callback-based, like Page Visibility API. There are also asynchronous APIs. A good example is the Permissions API. Most of the time, Tanstack Query is used to perform API requests. The useQuery
method is used to fetch data, whilst useMutation
is used to send data from the client to the server. However, Tanstack Query is a library that can handle any asynchronous operations. Here's an example of how Tanstack Query can be used to request permission to access the camera.
The key functionality lies in the useCameraPermission
hook.
const useCameraPermission = () => {
const {
data: isCameraPermissionEnabled,
isLoading: isCheckingCameraPermission,
isSuccess: isCheckingCameraPermissionSuccess,
refetch: askForCameraPermission,
} = useQuery({
queryKey: ["permissions-api"],
queryFn: async () => {
try {
await navigator.mediaDevices.getUserMedia({
video: true,
});
return true;
} catch (error) {
console.log("ERROR");
console.error(error);
return false;
}
},
enabled: false,
staleTime: Infinity,
retry: 0,
});
}
Instead of performing an API request in the queryFn
, we use the navigator.mediaDevices.getUserMedia
method to ask for permission to use the user's camera.
Tanstack Query executes queries immediately and is configured to re-execute the query callback after a certain time passes or if a user focuses the document. These are good defaults, but it's not something we want for this scenario. That's why we:
- disable the query using
enabled: false
- set
staleTime: Infinity
to prevent automated re-fetching - set
retry: 0
to prevent retries
Note that the enabled: false
config does not affect the refetch
method returned by useQuery
. Therefore, when we call askForCameraPermission
, which is the renamed refetch
, Tanstack Query will execute the queryFn
callback.
That's how Tanstack Query can be used with async Web APIs. Next, let's have a look at how it can be used with IndexedDB.
Tanstack Query and IndexedDB
Local storage is one of the most common solutions that is used to persist data locally. However, did you know that browsers offer a built-in database? IndexedDB is a low-level API for client-side storage that can hold a significant amount of structured data. Compared to local storage, which has a restriction of up to 10 MB, IndexedDB volume is mostly restricted by the user's available disk space. Therefore, it can hold much more data. Tanstack Query can be used to handle data fetching and submission to IndexedDB. The interactive example below has a simple application that persists a list of fruits in IndexedDB.
We use idb, a wrapper library around IndexedDB, which makes it easier to deal with IndexedDB. We establish a connection with the database in the useIndexedDb
hook.
import { useQuery } from "@tanstack/react-query";
import { type OpenDBCallbacks, type DBSchema, openDB } from "idb";
export const useIndexedDb = <DBTypes extends DBSchema | unknown = unknown>(
name: string,
version?: number,
config?: OpenDBCallbacks<DBTypes>
) => {
const {
data: indexedDb,
isLoading: isConnecting,
isSuccess: isDbReady,
} = useQuery({
queryKey: ["indexed-db"],
queryFn: async () => {
try {
const db = await openDB(name, version, config);
return db;
} catch (error) {
console.error(error);
throw error;
}
},
staleTime: Infinity
});
return {
indexedDb,
isConnecting,
isDbReady,
};
};
The indexedDb
instance is returned by useIndexedDb
and used in the useFruitsStore
hook.
The useFruitsStore
hook has queries and mutations to fetch, add and delete fruits.
const { data: fruits, isLoading: isFruitsLoading } = useQuery<
Array<{
fruitid: string;
name: string;
}>
>({
queryKey: ["fruits-query"],
queryFn: async () => {
try {
if (!indexedDb) {
return [];
}
const fruits = await indexedDb.getAll(FRUITS_STORE);
return fruits || [];
} catch (error) {
console.error(error);
throw error;
}
},
enabled: isDbReady,
});
The query to fetch fruits is enabled only when the IndexedDB instance is ready (enabled: isDbReady
). The fruits are fetched inside of the query callback: const fruits = await indexedDb.getAll(FRUITS_STORE);
. If there is any fruits data, it is returned, or we default to an empty array.
Next, we have two mutations. One to add a fruit and another one to delete it.
const { mutateAsync: addFruit, isPending: isAddFruitPending } = useMutation({
mutationFn: async (fruit: string) => {
if (!indexedDb) {
throw new Error("FRUITS STORE NOT READY");
}
await indexedDb.add(FRUITS_STORE, {
fruitid: Math.random(),
name: fruit,
});
},
onSuccess() {
queryClient.invalidateQueries({
queryKey: ["fruits-query"],
});
},
});
const { mutateAsync: deleteFruit, isPending: isDeleteFruitPending } =
useMutation({
mutationFn: async (fruitId: string) => {
if (!indexedDb) {
throw new Error("FRUITS STORE NOT READY");
}
await indexedDb.delete(FRUITS_STORE, fruitId);
},
onSuccess() {
queryClient.invalidateQueries({
queryKey: ["fruits-query"],
});
},
});
All the hooks and values returned by the useFruitsStore
hook are used in the IndexedDb.ts
file.
function IndexedDb() {
const [fruit, setFruit] = useState("");
const {
isLoading,
isReady,
addFruit,
fruits,
deleteFruit,
isAddFruitPending,
} = useFruitsStore();
return (
<div>
{/* jsx */}
</div>
)
}
React - The Road To Enterprise
Do you want to know how to create scalable and maintainable React apps with architecture that actually works?
Find out moreTanstack Query and Async Libraries
We will use localforage to demonstrate how to utilise Tanstack Query with async libraries. The localforage library is a fast and simple storage wrapper around IndexedDB and WebSQL with a localStorage
-like API. If a browser does not support IndexedDB or WebSQL, it will fallback to using localStorage
. We can use it with Tanstack Query to fetch and write data to whichever storage solution is available in the current browser. The interactive example below showcases the same functionality as the example in the previous section, but this time using localforage
instead of idb
.
In the useFruitsLocalStore
hook we have a query to read the fruits data using localforage
and two mutations for adding and deleting fruits.
const { data: fruits = [] } = useQuery({
queryKey: ["fruits-query"],
queryFn: async () => {
const fruits = await localforage.getItem<Array<Fruit>>("fruits");
return fruits || [];
},
});
The read fruits query is very simple, as it just reads fruits
data and returns it. If there is no data in the storage, then it returns an empty array.
In the add fruit mutation, we use the localforage.setItem
method to combine current fruits with new one.
const { mutateAsync: addFruit, isPending: isAddFruitPending } = useMutation({
mutationFn: async (fruit: string) => {
await localforage.setItem("fruits", [
...fruits,
{
fruitid: Math.random(),
name: fruit,
},
]);
},
onSuccess() {
queryClient.invalidateQueries({
queryKey: ["fruits-query"],
});
},
});
Upon successful submission, we invalidate the fruits-query
, so Tanstack Query refetches the latest data from the storage.
Last but not least, in the delete fruit mutation, we filter out a fruit by its ID and invalidate fruits-query
in the onSuccess
callback.
const { mutateAsync: deleteFruit, isPending: isDeleteFruitPending } =
useMutation({
mutationFn: async (fruitid: string) => {
await localforage.setItem(
"fruits",
fruits.filter(fruit => fruit.fruitid !== fruitid)
);
},
onSuccess() {
queryClient.invalidateQueries({
queryKey: ["fruits-query"],
});
},
});
Tanstack Query and Web Workers
Here's another interesting use case for Tanstack Query. Have you ever tried using Tanstack Query with Web Workers? This can be a bit tricky, as Web Worker's API is not promise-based. Messages to the worker can be sent using worker.postMessage
method. We can subscribe to messages from a worker using the worker.onmessage
handler or we can register one using worker.addEventListener()
. So, how can we combine Web Workers with Tanstack Query? How about promisifying a Web Worker? Fortunately, there is a library called Comlink that can do just that.
The interactive example below showcases how to combine Tanstack Query with the Comlink library to sort a list of users in a Web Worker.
You can click on the Ascending
and Descending
buttons to change how users are sorted.
In the WebWorker.ts
file, we export contents for the worker script. The object that is exposed via comlink
has one method - sortUsers
. It sorts users array by email in ascending or descending order. The worker script is first used to create a blob.
const workerBlob = new Blob([workerString], {
type: "text/javascript",
});
Next, we create a URL for the worker blob.
const workerUrl = URL.createObjectURL(workerBlob);
The worker URL is then passed to the Comlink.wrap
method.
const worker = Comlink.wrap<{
sortUsers(data: { sortBy: SortBy; users: Array<User> }): Array<User>;
}>(new Worker(workerUrl));
Finally, we call the worker's sortUsers
method inside of the queryFn
callback.
const { data: users = [] } = useQuery({
queryKey: ["users", sortBy],
queryFn: async () => {
return worker.sortUsers({
users: usersData,
sortBy,
});
},
});
That's how we can use Tanstack Query to communicate with a Web Worker.
Summary
Tanstack Query is a powerful async state manager library. Even though it is used most commonly for handling server state via API requests, it can be used for much more than that. In this tutorial, we have covered how to use it to communicate with async Web APIs, libraries and even workers. Did you find any interesting use cases for Tanstack Query? Drop me a message on X or on email at support@theroadtoenterprise.com.