#How do I handle a Promised returned by a function in loader()?

1 messages · Page 1 of 1 (latest)

frigid dust
#

Hello! So I have a react/SPA app that pulls files from Cloudflare's R2 service and it works fine. I am trying to port that over to react-router (using the same Vite plugin). The R2 get() seems to be working fine, but I can't work out the secret sauce to process the returned R2ObjectBody. According to the docs here (https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2objectbody-definition) .blob() returns a promise. I deal with this promise directly in fetch() in javascript, but I am unsure how so set this up in the loader() function?

export async function loader({ context }: Route.LoaderArgs) {

  const r2Map: R2ObjectBody = await context.cloudflare.env.BUCKET.get(key);
  if (!r2Map) {
    throw new Response("R2 object not found");
  }

  r2Map.blob()
    .then((mapBlob) => URL.createObjectURL(mapBlob))
    .then((mapImage) => {
      return { mapImage };
    })
}

export default function Maps({ loaderData }: Route.ComponentProps) {

  return (
    <>
      <div>
        <img src={loaderData.mapImage} />
      </div>
    </>
  )
}

Of course loaderData in the <img> tag is possibly undefined, which I understand. I think I am missing something simple. In a react SPA I would use setMapImage() to set this value. How do I make this happen in loader()?

Cloudflare Docs

The in-Worker R2 API is accessed by binding an R2 bucket to a Worker. The Worker you write can expose external access to buckets via a route or manipulate R2 objects internally.

#

I ask in here because I'm fairly certain I could write my own function to load the data on this route, but I don't want to fight the framework. Yes, my promise-foo is not strong.

weary dune
#

There are a couple ways to do it. You can do it in a single component using <Await> like this:

import { Suspense } from 'react';
import { Route } from './+types/_._index';
import { Await } from 'react-router';

export async function loader({ context }: Route.LoaderArgs) {
  const r2Map: R2ObjectBody = await context.cloudflare.env.BUCKET.get(key);
  if (!r2Map) {
    throw new Response('R2 object not found');
  }

  return {
    mapImage: r2Map.blob().then((mapBlob) => URL.createObjectURL(mapBlob)),
  };
}

export default function Maps({ loaderData }: Route.ComponentProps) {
  return (
    <Suspense fallback={<>Loading...</>}>
      <Await resolve={loaderData.mapImage}>
        {(url) => (
          <div>
            <img src={url} />
          </div>
        )}
      </Await>
    </Suspense>
  );
}

You can also separate into two separate components and relly on React.use:

import { Suspense, use } from 'react';
import { Route } from './+types/_._index';

export async function loader({ context }: Route.LoaderArgs) {
  const r2Map: R2ObjectBody = await context.cloudflare.env.BUCKET.get(key);
  if (!r2Map) {
    throw new Response('R2 object not found');
  }

  return {
    mapImage: r2Map.blob().then((mapBlob) => URL.createObjectURL(mapBlob)),
  };
}

export default function Maps({ loaderData }: Route.ComponentProps) {
  return (
    <Suspense fallback={<>Loading...</>}>
      <Image src={loaderData.mapImage} />
    </Suspense>
  );
}

function Image({ src }: { src: Promise<string> }) {
  const url = use(src);
  return (
    <div>
      <img src={url} />
    </div>
  );
}
#

The trick is that you need to return a Promise itself as a field on your loader data

#

if your loader returns Promise itself, vs an object containing a Promise, then RR will await it before rendering your component

frigid dust
#

Oh, wow. I was not aware of Await. Very new to framework mode! Thanks so much for this code. Let me work on it for a bit.

frigid dust
#

Hmm. I still get the Error: URL.createObjectURL() is not implemented error - I think that means mapBlob is not yet defined. Also, I like the Await code. Very clean.

#

I see the Loading... message very briefly, then a generic worker error message. The not implemented error is in the logs.

#

Hmm.

app/routes/maps.tsx:7:9 - error TS2322: Type 'R2ObjectBody | null' is not assignable to type 'R2ObjectBody'.
  Type 'null' is not assignable to type 'R2ObjectBody'.
#

Does not block the build but does not pass typecheck.

#

Meh, that's just a TS issue. Can clear it but the URL error persists.

weary dune
frigid dust
#

Huh. That's interesting, as this works fine in a javascript react/SPA:


  const getR2Data = (r2Url) => {
    // use /api simply so the arg can be read correctly by searchParams
    const url = "/api?r2Image=" + r2Url;

    fetch(url)
      .then((res) => res.blob())
      .then((blob) => URL.createObjectURL(blob))
      // Update image
      .then((binaryData) => setBinaryData(binaryData))
      .catch((err) => {
        console.log("R2 fetch error:");
        console.log(err);
      });
  };

I wonder if it's because it is running server-side at this point?

#

I may have to move to clientLoader()?

#

Wow. Subtle.

#

No, that would upend the whole design. Perhaps I need to run the createObjectURL() in the component code, not in loader().

#

Afk for a bit - I will get back to this this evening. Thanks for staying tuned @weary dune 👍

weary dune
#

Yeah loader runs server-side, so support for URL.createObjectURL depends on what runtime you're running on the server

frigid dust
#

Wow. This is rather a bit more difficult than I expected.

#

I think the framework approach may not be a good choice, here.

#

This is silly silly easy as an SPA, I just need some simple routing.

weary dune
#

You can still use framework mode with ssr: false and build a fully-static SPA

frigid dust
#

Yes I'm going to try that. It's not as trivial as converting loader() -> clientLoader() though, so I need to do a little research into how the CF vite plugin works.