Retrieving Files As Blob in React Native (and Expo)

tl;dr
Sometimes you may need to create a valid blob file from a remote file or a local file (for example, for uploading a screenshot to firebase storage). If the Blob object returned by the fetch API (e.g. blob = await fetch(`file://${local_uri}`).blob()) is returning an empty type and/or 0 bytes, then upgrading to React Native 0.59.x+ (expo sdk 33+) should fix the issue. If upgrading is not a possibility, there are other options such as creating a (hacky) blob utility function, or passing around base64 encoded data (see below).

I want to note that if you simply want to use an Image component to display the local file in the UI, you can specify the local location of the file as the source.

Note also that I am not referring to the situation where your file can be packaged with your app ahead of time or where it can be directly loaded into the UI from the internet. If your file is being downloaded from the internet it can usually be loaded directly via URL. If it is something that you can package with your app, the best practice is to simply use an import or require to include the file at compile time.

However, for more complicated use cases, such as uploading the file, it can get quite a bit trickier.

Uploading files that are dynamically created in-app (such as a screenshot), for example, can be tricky. Also, fetching any file prior to React Native 0.59.x with the fetch API has issues associated with it.

If you are not using Expo, it is worth looking into rn-fetch-blob. Unfortunately, since I was using Expo, I could not use rn-fetch-blob without detaching and using ExpoKit (something I would like to avoid for a task that should be relatively simple).

In my case, uploading a dynamically created tempfile to firebase took quite a bit of effort. I would like to share a couple of solutions I came up with.

The main problem for me came down to an issue with React Native’s custom whatwg-fetch polyfill in versions prior to 0.59. RN’s custom polyfill specifically does not use a ‘blob‘ response type (blob support was added to React Native and Expo in early 2018). Therefore, fetching files with it (vs, for example, fetching JSON) caused an issue where the resulting blob would have 0 bytes and an empty string as the ‘type’. Let me demonstrate with an example using a basic expo app.

We will start by attempting to download a jpg from the internet as a blob, and then we will output the fetch Response object and the Blob object that results from calling blob() on it (note that fetch’s Response.blob() returns a promise, so we have to await or resolve it to get the actual object):

App.js:

import React from 'react';
import { Text, View } from 'react-native';

const getFile = async () => {
  const img_url = "https://picsum.photos/200/300.jpg";
  let result = await fetch(img_url);
  console.log(result);
  console.log(await result.blob());
  return result;
}

export default function App() {
  getFile();
  return (
    <View>
      <Text>Open up App.js to start working on your app!</Text>
    </View>
  );
}

And the data looks like this:

Notice that the size of the Blob object is 0 and the type is an empty string.

We can try something similar by fetching JSON and calling json() on the Response object, but this time we get the expected JSON output:

const getFile = async () => {
  const json_url = "https://jsonplaceholder.typicode.com/todos/1";
  let result = await fetch(json_url);
  console.log(result);
  console.log(await result.json());
  return result;
}

And we get exactly what we expected – a javascript object with the correct JSON values.

Additionally, we can upgrade to expo SDK 33, which uses React Native 0.59 and we also get what we expect:

App.js (Expo SDK 33 / React Native 0.59):

import React from 'react';
import { Text, View } from 'react-native';

const getFile = async () => {
  const img_url = "https://picsum.photos/200/300.jpg";
  let result = await fetch(img_url);
  console.log(result);
  console.log(await result.blob());
  return result;
}

export default function App() {
  getFile();
  return (
    <View>
      <Text>Open up App.js to start working on your app!</Text>
    </View>
  );
}

Success! We have successfully fetched a file as a valid JS Blob object (note the correct type/size info). Now we can use it in the large number of javascript File API’s that accept Blobs.

So what is happening here?

For very specific reasons, prior to version 0.59 React Native had a custom whatwg-fetch polyfill implementation that specifically does NOT return a blob responseType by default.

However for these reasons that custom polyfill has become unnecessary. In version 0.59 it was altered to return a responseType of ‘blob’ by default (which is what enables the Response.blob() function to work correctly). And then after 0.59 the custom polyfill was removed altogether as it was now redundant.

The simple solution is to upgrade to React Native 0.59 or higher (or Expo SDK 33, the highest at the time of this writing).

After upgrading to RN 0.59 my resulting Blob objects have a type of ‘image/jpeg’ and a correct size value. More importantly, now they properly upload to Firebase storage as valid images without any problems.

But what if upgrading is not an option?

If you are downloading the file from the internet you can write a urlToBlob utility that uses XMLHttpRequest to grab the file as a blob:

function urlToBlob(url) {
  return new Promise((resolve, reject) => {
      var xhr = new XMLHttpRequest();
      xhr.onerror = reject;
      xhr.onreadystatechange = () => {
          if (xhr.readyState === 4) {
              resolve(xhr.response);
          }
      };
      xhr.open('GET', url);
      xhr.responseType = 'blob'; // convert type
      xhr.send();
  })
}

If you are creating a screenshot using takeSnapshotAsync (as I was) you also have the option to set the result as a ‘data-uri’ to get a base64 encoded data version of the file. This base64 data can then be turned into a blob or, in the case of Firebase, uploaded directly as a data_url encoded string.

I found that the base64 string created by takeSnapshotAsync was exceptionally large compared to the actual image, so I chose to utilize an actual image, storing it in the local cache, grabbing it with fetch and then uploading it to firebase from there.

Retrieving Files As Blob in React Native (and Expo)

Leave a Reply

Your email address will not be published. Required fields are marked *