---
title: "useAsyncFetcher: How to implement an asynchronous fetch function in a Remix app"
description: "Presenting remix-use-async-fetcher, a technique and library for implementing an asynchronous fetch function in a Remix application by leveraging clientLoader. Clear explanations, code examples, a functional library, and a demo app are all included!"
author: "Hasan Ayan"
date: "2025-01-16T16:12:44.929Z"
canonical: "https://www.cerbos.dev/blog/useasyncfetcher-implement-asynchronous-fetch-in-remix"
image: "https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/use_Async_Fetcher_header_08c31c2c12.png"
tags: ["engineering"]
source: "https://www.cerbos.dev/blog/useasyncfetcher-implement-asynchronous-fetch-in-remix"
---

# useAsyncFetcher: How to implement an asynchronous fetch function in a Remix app

> [!NOTE]
> If you'd like to skip the reading part, we have created a package called `remix-use-async-fetcher`. It's on [GitHub](https://github.com/cerbos/remix-use-async-fetcher) and it contains what you need to call server loader and actions with a Promise return.
> 
> Although not yet published on npm, you can install it directly from git. If you wish to play with the demo app, visit [this StackBlitz page](https://stackblitz.com/github/cerbos/remix-use-async-fetcher).


The [Backend for Frontend](https://remix.run/docs/hi/main/guides/bff#backend-for-your-frontend) concept in Remix and its associated hooks such as `useLoaderData` or `useFetcher` are great tools for building dynamic pages. In most cases, these hooks provide a simple and reliable way of getting data from loaders. However, there are some cases where this concept imposes certain limitations, and while working on our Audit Logs feature, we hit one of them. In this article, I'll tell you about that limitation and show you how we implemented a solution for ourselves.

Since we wanted to implement bi-directional infinite scrolling for the Audit Logs page, we needed to be able to cache the loader data. Sure, you can use the existing Remix data hooks like `useFetcher` with some `useState` and `useEffect` magic, but implementing caching and invalidation can become complex depending on the requirements and you are likely to revisit your implementation when you have new requirements. This feels like reinventing the wheel when `@tanstack/query` already exists—a package for managing query caching that has been widely adopted by the React community for years. So, we wanted to use `@tanstack/query`. Unfortunately, `@tanstack/query` requires an async function to fetch the data and Remix loaders do not natively support that. 

Using the native `fetch` function to retrieve loader data is an option, but if you are using [Single Fetch](https://remix.run/docs/en/main/guides/single-fetch#single-fetch), it requires handling the deserialization of `TurboStream` responses. This is not hard to figure out from looking at the Remix source but it's undocumented and there is no guarantee for compatibility with future Remix versions. We want to stick to "Remix way of doing things" and that's why we eliminated this option.

The good news is that Remix gives us `clientLoader` and `clientAction`. In this post, we will focus on `clientLoader`.

## How does `clientLoader` work?
When you call `useFetcher().load("my-route")`;
- Without a `clientLoader`, Remix calls the `loader` of `my-route` and updates the state of the `useFetcher` call with the data returned.

<pre class="mermaid">
sequenceDiagram
    participant Component
    participant useFetcher
    participant loader

    Component->>useFetcher: call load("my-route")
    useFetcher->>loader: call loader of "my-route"
    loader-->>useFetcher: return data
    useFetcher-->>useFetcher: update state with data
    useFetcher-->>Component: re-render with the updated state
</pre>

- If you have a `clientLoader` next to the `loader` of `my-route`, Remix calls `clientLoader` with request context, and an async `serverLoader` function to get the `loader` and data. What you return from your `clientLoader` will be used to update the state of the `useFetcher` call.

<pre class="mermaid">
sequenceDiagram
    participant Component
    participant useFetcher
    participant clientLoader
    participant loader

    Component->>useFetcher: call load("my-route")
    useFetcher->>clientLoader: call clientLoader with context
    clientLoader->>loader: call serverLoader for data
    loader-->>clientLoader: return data
    clientLoader-->>useFetcher: return data to update state
    useFetcher-->>useFetcher: update state with data
    useFetcher-->>Component: re-render with the updated state
</pre>

Remix presents this feature as a way to cache loader responses. In a `clientLoader`, you can decide if you'd like to serve the data from the cache or hit the backend. However, since we want `@tanstack/query` to handle caching, we will use `clientLoader`s only to create an async fetch function.

## How can `clientLoader` be used to create an async fetch function?

Here is the sequence diagram of what we are going to implement:

<pre class="mermaid">
sequenceDiagram
    participant Component
    participant useAsyncFetcher
    participant useFetcher
    participant clientLoader
    participant loader

    Component->>useAsyncFetcher: call async fetch function with "my-route"
    useAsyncFetcher->>Component: create Promise and return it
    useAsyncFetcher->>useFetcher: call original useFetcher().load with "my-route?__request-id=[unique-id]"

    useFetcher->>clientLoader: call clientLoader
    clientLoader->>clientLoader: check request URL for __request-id
    clientLoader->>loader: call serverLoader for data
    loader-->>clientLoader: return data

    alt Data fetch successful
        clientLoader->>Component: resolve Promise associated with __request-id with data
    else Data fetch failed
        clientLoader->>Component: reject Promise associated with __request-id with error
    end
</pre>

We will:
- Create a wrapper around `useFetcher().load` which will return a promise created by [`Promise.withResolvers()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) and then call the original `useFetcher().load` function after appending a unique ID as a URL search parameter keyed by `__request-id`. 
- Store this request ID and the associated `Promise.withResolvers()` return somewhere globally accessible.
- Add `clientLoader` functions for the loaders we would like to be able to call asynchronously. They will:
    - Check the request URL in `clientLoader` and see if it has `__request-id`.
    - Call `serverLoader` to fetch the data.
    - If data fetching is successful, resolve the `Promise` associated with the `__request-id` with the data, otherwise, reject it with an error.
    - Return the resolved data to ensure Remix's data hooks function as expected.

And there you have it, an asynchronous function for fetching loader data in Remix.

## Coding time!

In `use-async-fetcher.ts` file;

First, define the `Map` that will hold what `Promise.withResolvers()` returns for each request.

```typescript
const asyncFetcherQueries: Map<string, PromiseWithResolvers<unknown>> = new Map<
  string,
  PromiseWithResolvers<unknown>
>();
```

Next, define `useAsyncFetcher` that returns an async fetch function, preferably within the same file.

```typescript
const requestIdKey = "__request-id";

export function useAsyncFetcher(): <T>(href: string) => Promise<T> {
  const originalFetcher = useFetcher();

  const fetch = useCallback(
    async <T>(href: string): Promise<T> => {
      const requestId = crypto.randomUUID();

      // append the request ID
      href = href.includes("?")
        ? `${href}&${requestIdKey}=${requestId}`
        : `${href}?${requestIdKey}=${requestId}`;

      const promiseWithResolvers = Promise.withResolvers<T>();

      // store promiseWithResolvers keyed by request ID
      asyncFetcherQueries.set(
        requestId,
        promiseWithResolvers as PromiseWithResolvers<unknown>,
      );

      // initiate loader call
      originalFetcher.load(href);

      // return the promise.
      return await promiseWithResolvers.promise;
    },
    [originalFetcher],
  );

  return fetch;
}
```

> [!WARNING]
> For each request made through `useAsyncFetcher`, we are adding a search parameter to the route. Using request URLs directly when building redirect URLs might be problematic. If you don't sanitize the redirect URL, this may inadvertently redirect the client to a URL containing `__request-id`

So far we've managed to initiate a request, store the associated `Promise` and return it. Now, we need to add `clientLoader` for each `loader` we want to be able to call asynchronously. Let's add a helper for that.

```typescript
export async function handleClientLoaderForAsyncFetcher({
  request,
  serverLoader,
}: ClientLoaderFunctionArgs): Promise<unknown> {
  const { searchParams } = new URL(request.url);
  const requestId = searchParams.get(requestIdKey);

  try {
    // call the server loader
    const serverResponse = await serverLoader();

    // if request ID is present, resolve the promise with the server data
    if (requestId) {
      // This is undocumented, but if you do a redirect in the loader,
      // `serverLoader()` will resolve to a Response object.
      // When we encounter a Response object we treat that as a fetch
      // error. Since we are planning to use @tanstack/react-query
      // we can rely on it retrying the request afterwards.
      if (serverResponse instanceof Response) {
        asyncFetcherQueries.get(requestId)?.reject(
            new Error("Encountered a Response object")
        );
      } else {
        asyncFetcherQueries.get(requestId)?.resolve(serverResponse);
        asyncFetcherQueries.delete(requestId);
      }
    }

    // Return the data to ensure Remix's
    // data hooks function as expected.
    return serverResponse;
  } catch (e) {
    if (!requestId) {
      // rethrow when there is no Request ID
      // to ensure Remix's data hooks function
      // as expected.
      throw e;
    }

    asyncFetcherQueries.get(requestId)?.reject(e);
    asyncFetcherQueries.delete(requestId);
    return null;
  }
}
```

DONE! Now we have an async fetch function to call the loaders. Here is a simple example of how `useAsyncFetcher` is used.

```typescript
export async function loader() {
  return json({ name: "Ryan", date: new Date() });
}

export const clientLoader = handleClientLoaderForAsyncFetcher;

export default MyPage(){   
    const fetch = useAsyncFetcher();
    const [data, setData] = useState(undefined);
    const [error, setError] = useState(undefined);
    const [isLoading, setIsLoading] = useState(true);
    
    useEffect(()=>{
        fetch(/account")
        .then(data=>{
            setData(data);
            setIsLoading(false);
        })
        .catch(error=>{
            setError(error);
            setIsLoading(false);
        })
        
    },[]);
    
    return <>...</>
}
```

That's it. `useAsyncFetcher` is all you need to be able to cache `loader` data using `@tanstack/react-query`. If you need to use `useMutation` from `@tanstack/react-query`, you can apply the same principle to `useFetcher().submit` & `clientAction`.
