Using Images in React Native

Allan Graves
Dec 29, 2020 · 16 min read
Image for post
Image for post
Photo by Alexander Andrews on Unsplash

A few hours ago, I was working on a piece of React Native code that required dynamic image exports off the file system. Of course, I started with the React Native Image document: https://reactnative.dev/docs/image

React Native has some great docs, so after reading this, I figured it would be a piece of cake.

Specifically, what I wanted to do was have a JSON file with data, and have a list of images in that data with information about them. I didn’t know how many images, or how much data ahead of time.

After perusing the React Native docs, I realized that there are several ways to include images, all of them pretty self explanatory…. but none of them would really let me read images directly from JSON on a local file system without some extra work.

First, there’s the old and classic, ‘require’:

<Image style={{height: 50, width: 50}} source={require('./assets/images/photo1.jpg')} />

According to the React Native documents, this does several things:

  1. This uses the CommonJS function ‘require’, which Metro looks for to know when to place the photo in our bundle. It sets up the Metro bundler (also known as the packager or Metro Server) to include the file photo1.jpg into the bundle, based on the existence of the ‘require’ keyword.
  2. Metro converts the asset (jpg file) into an object that can be displayed by an <Image /> component.
  3. Require itself returns an Object, which is used as the source for the image.
  4. The Object in this case is an integer which Metro resolves at Image load time into a location.

Okay, that one was easy.

It’s not very dynamic though.

It’s nice because every code update to use a new image automatically gets the image built into the bundler, and transferred over to the device. You don’t have to worry about file system paths or uri or anything like that. Update the image in your code, and boom, you update the image in the bundle.

This would be nice if we had a list of files, like a splash screen and some other assets that we wanted to display. We definitely don’t want to download them, the user should be able to use the app after they install it with no extra fuss.

We can make it a BIT more dynamic by doing something like this:

const DATA = [
{
text: “man”,
image: require(‘./assets/images/photo1.jpg’)
},
{
text: “woman”,
image: require(‘./assets/images/photo2.jpg’)
}
]

This stores the information in an array. Note that the ‘require’ is NOT a text string. It is still an executed code line that will return an object — so the image property of this JS object is another JS object.

Don’t believe me? Log the output of the image property:

[Info] 12–24 06:02:42.359 27376 27440 I ReactNativeJS: 1

This is a reference to the asset, and can be used in place of the DATA[0].image property— you could set <Image source={1} /> directly.

Now, moving back to our App, we added the following code:

So, what happened here?

  1. Line 1–10, we created a DATA array to hold our images and text.
  2. Line 13 — we used the React State mechanism to store state of the current imageVar. This returns an array with 2 variables — the first, is imageVar, the current image. We initialize this variable with the useState(0) function, to set the state to 0. This is a special function called a Hook, which allows you to hook into a React Feature, adding state to it. Secondly, it returns setImageVar, a function which is used to access the state and change the value. This is equivalent to a setattr.
  3. Then, on line 19, we setup an Image component, and assign the initial source to be the object in DATA[imageVar].image. The imageVar value defaults to 0 here. The image property is the object returned by the ‘require’ statement — in this case, an int into the index of assets.
  4. Line 20 does the same thing for the text.
  5. Line 21 sets a Button component up — this button uses the setImageVar function returned earlier to adjust the value. (explicitly, if imageVar is the same as our array size, we reset to 0, otherwise we add 1)

The magic here is that React Native only rerenders components that have changed to improve performance. In this case, our use of the React State triggers a rerender on just the component that has had its state changed — the Image and Text buttons here. Think of it like a giant callback that says “I’m registering as using this piece of state. When it changes, I need to be notified.”

Again, you still have to specify every image you’re loading in this array as a require, and include this array somewhere. Nothing gets picked up automatically because it is in a certain directory, but there’s no build steps to worry about, any image you need in code is automatically included in your bundle. Should you need to change the images, simply change your code list of requires, and the new images will be picked up by the Image component without tracking down 10 different places. This is great for code maintenance.

Image for post
Image for post

Okay, we’ve managed to load pictures dynamically through an array, but… we have to point out an issue.

Examining our array closely, we can see that the ‘image’ property is still an object — that is, Babel can still translate this ‘require’ call into a JavaScript object, ensuring that the image is loaded into the static assets.

The difference between this and a JSON file is that a JSON file needs to have its data escaped.

const DATA = ‘{ “image1”: { “text”: “man”, “image”: “./assets/images/photo1.jpg” }, “image2”: { “text”:”woman”, “image”:”./assets/images/photo2.jpg” }}’;

This is a messy way of saying…. we have a set of name \ value pairs.

“image1”: {

Notice that we’ve taken the requires out. The requires would not help us — JSON is name\value pairs, so it looks like just a string to the Metro Packager. This means that we don’t have our file on the system — it’s no longer being packaged up.

We’ll need to find a way to get that file on the system.

In the meantime, let’s go ahead and parse out the JSON:

let parsedData = JSON.parse(DATA);

And done! Now we can simply do something like this:

console.log(parsedData.image1.text);

And get this response:

[Info] 12-24 09:31:11.988 21250 22480 I ReactNativeJS: man

Viola!

Changing our Image component to:

<Image style={{height: 50, width: 50}} source={parsedData[imageVar].image}/>

Results in this error:

[Info] 12-24 09:35:19.832 21250 22480 E ReactNativeJS: TypeError: undefined is not an object (evaluating 'parsedData[imageVar].image')

This is because the source property should be an object, instead, we are passing a string directly from our JSON parsing. (Remember, the source object ended up being an integer pointing to the index of the resource.)

To use a string, we need to use something called a URI, or “Uniform Resource Identifier”. You actually use this all the time — in your web browser, this is what you use when you go to “http://”. It tells the browser that this is a “http” type. You can try “file://” for a local file there.

React Native supports the same thing.

First though, we need to see where the static resources are kept. To do that, we’ll want to look at the basic file system for our app.

Let’s start with the old venerable react-native-fs, which was the proper way to access react-native file systems:

npm ERR! Could not resolve dependency:
npm ERR! peer react-native@"^0.59.5" from react-native-fs@2.16.6
npm ERR! node_modules/react-native-fs
npm ERR! react-native-fs@"*" from the root project

Ooops. Well, looks like we can’t use react-native-fs anymore with the latest version of react. Or perhaps it needs updating. From the Issues pages, it’s hard to know.

Anyway — we’re going to move to Expo libraries for a bit — we’ll see why later, but let’s just get this out of the way for now:

To do so, we’ll turn to the Expo Filesystem module. Except, to install it in a React-Native project, you need to use the unibrow module… I mean the unimodules. https://docs.expo.io/versions/latest/sdk/filesystem/#supported-uri-schemes-1

It looks scary, but it’s pretty simple. Once you do this, your project may not work.

I had to load Android studio, relink the project there, then restart the packager and my Android Emulator, which is pretty consistent with what happens when you add a new package.

The File system module gives us access to a call — readDirectoryAsync().

Let’s use this to see what’s going on in our file system:

This simple button can be embedded somewhere in your code — and will return output like this:

[Info] 12–28 05:44:41.645 7127 10775 I ReactNativeJS: Reading :file:///data/user/0/com.uritest/files/

Huh. Not what we expected.

I would have expected more assets there — I mean, we have the following line in our code:

const foo = require(‘./assets/images/photo1.jpg’);

Well, okay — so let’s dig a bit more. Where is React Native getting the assets from?

React Native image library contains the following call: Image.resolveAssetSource().

This call allows us to see the URI behind any static asset (in this case, Image) in our bundler.

Let’s give that a shot:

const foo = require(‘./assets/images/photo1.jpg’);

Prints out:

[Info] 12–28 05:54:20.289 7127 10775 I ReactNativeJS: FooURI: http://10.0.2.2:8081/assets/assets/images/photo1.jpg?platform=android&hash=25052dcebf6333bef4fa5c380130eccd

Which actually makes sense. This is an emulator, not a published app, ready for deployment. In deployment, this would resolve to a local resource, but in development, we want to be able to change things quickly. So everything is resolved off the local web server, which explains the local file system being blank.

With that said, let’s download the resource locally, so we can load it in using a URI.

The above code does a couple of things:

  1. Uses the Image.resolveAssetSource() call to get the URI of this particular asset.
  2. Downloads the file to a local resource — the download call takes a URI and local file URI.
  3. Uses the passed in {uri} return parameter of the downloadAsync function to print where the file was downloaded to. This should match the URI we passed in.
  4. Reads the directory to see if we got the file.
[Info] 12–28 06:24:46.329 7127 10775 I ReactNativeJS: Using :file:///data/user/0/com.uritest/files/

And, as we can see, it worked!

The directory read at the end of our function shows that ‘photo1.jpg’ is clearly available here.

Note — local File URI being specified must already have all directories created — that is, if you specify DocDir+”/dir1/dir2/a.jpg”, then “dir1/dir2” must already exist.

Next, let’s load that picture in.

For that, we’ll use the following code:

Let’s pull out some of the highlights here.

Ignore Lines 3–6: They are getting a URI locally. We could have easily downloaded any URI from anywhere. In fact, when working with http, we can just load the image from that URI directly. I’m only using this as an easy way to get the file on the device for this step.

First, we don’t want to render an Image component when we have no Image downloaded. In order to do that, we:

  1. On Line 8, set a new state hook which gives us the ability to know if we’ve downloaded the image. We initialize it to 0 (False).
  2. On Line 12, we have dlImage — an object that is assigned conditionally, based on the state, to either some placeholder text or the actual image.
  3. On line 52, we include the JavaScript to setup a component reference for dlImage — which will render based on the state of imageDownloaded. This allows us to conditionally render an image.
  4. On line 30, after we download the image, we set the state of imageDownloaded to 1.
  5. When the user clicks the button, this rerenders the App component (registered as using this particular state variable), and this time, the Image we just downloaded is included.
Image for post
Image for post
Pretty Cool, eh? It’s all dynamic and stuff!

After all this, we have the ability to download files locally, and load Images (or other assets) in using the URI.

Line 10 sets the image URI to be the local file.

Line 13 loads this local file. The magic is “file://” which is the local file system URI indicator. Then, after that, we need to provide a path that exists, and that the app has access to.

That’s a big step forward, but let’s not lose site of our original goal — we want to be able to take image files directly from our JSON file and load them in, not using a resource location as specified by the requires.

Before we continue, let’s just do a quick experiment, just to show what is or isn’t in the bundle that is produced. This will help us understand what’s going on, and it’s good knowledge for later.

Make 2 directories above your React Native root, assets and bundle.

Then, run the following command from your React Native root, substituting your directories for d:\gitrepos\assets and d:\gitrepos\bundle. This is what would eventually be bundled up for a deployment build.

PS D:\gitrepos\uritest> npx react-native bundle — assets-dest d:\gitrepos\assets — entry-file d:\gitrepos\uritest\index.js — bundle-output d:\gitrepos\bundle\out.bundle — verbose
Welcome to React Native!
Learn once, write anywhere

If you don’t put a ‘require’ call in your project, we’ll see no assets\images directory under the d:\gitrepos\assets directory. (Or whatever directory you gave to the assets-dest option.

Ensure you’ve added in a call to set a require:

const foo = require('./assets/images/photo1.jpg');

And suddenly, there’s a ./assets/images/photo1.jpg file there!

While not the only way to verify, this is certainly one way! (For instance, we could have unpacked the android bundle, by heading to the localhost:8081 location. I just found this easier.)

Right now, at this point, we’ve done the following:

  • Shown that we can load images locally from the file system.
  • Shown that we can’t load images that we don’t have in our bundle.
  • Shown that the ‘require’ command doesn’t work from JSON.
  • Shown that the ‘require’ command returns an object.

This leaves us with a basic question: How do we get our assets transferred to the device so that we can use them — while specifying them in a JSON file, which doesn’t interpret the JSON require — so they exist locally and the user can use our app immediately?

There are multiple ways to solve this, so let’s go through each of them, and some of the advantages and disadvantages of them.

We *could* specify them as assets in Android and iOS, using the Gradle files to transfer them. This would treat them as OS Specific assets, and they would have to conform to the rules for each platform, including size, naming, characteristics.

It could be automated using a Gradle build script — which is nice. Ie — take them from this directory, and move them to these specific locations. This still gives us the ability to have a single source of Truth for our images — but the naming will change along the way, so reverse mapping an asset back to the source it came from would be harder.

If possible, I’d like to have the bundler properly pick them up — there doesn’t seem to be a need to have anything special done. This utilizes the ability to have the packager do what it is supposed to do.

For my purposes, I’m going to be using all files in my assets/images directory, so it should be okay to use the requires on everything in the directory.

Let’s try using ‘require’ dynamically:

for (var key in parsedData) {
console.log(parsedData[key].text);
imgObjects.push(require(parsedData[key].image));
}

This should be pretty obvious, but this is the error you get from that:

[Thu Dec 24 2020 10:44:55.200]  ERROR    TypeError: undefined is not an object (evaluating 'parsedData[imageVar].image')

Require is a compile time function, not a runtime function. That is, it doesn’t know anything about program flow or execution, it merely looks for modules that need to be loaded. (IE — the code is run *after* the Packager has run, and the code and assets are already on the Android device, .)

That won’t work, we need to get assets on the device.

Another way to work around this is to setup a separate index.js file and map via the use of a dictionary a ‘require’ to a key — then we can use the image that way. An index.js file is JavaScript code that is used to define a module for React Native. This allows you to specify the imports and exports and other defaults for a module.

However, this means that images need to be listed in 2 different locations, once in the JSON and once in the index.js.

Not ideal. It’s not ideal because then we have to maintain 2 separate listings — 1 in the JSON and 1 in the JavaScript source files. It also means we need to have a way to translate from one location listing to the ‘require’ object or index id.

I don’t like managing the same data in 2 separate locations — it always leads to a bad day trying to figure out why an image isn’t in one place or another. A mantra of an experienced developer — ie, one that has spent weeks trying to track down a particular error — is that code and resources should not be duplicated.

Note of course, with sufficient build magic the index.js could be built dynamically from JSON. Or JSON could be generated directly from the header. Or both could be generated from the image directory itself.

In fact, always remember — With sufficient build magic, almost anything can be overcome. It’s just more a question of whether build magic is the RIGHT thing to do.

As a benefit, this leaves us with all our images in a simple require format, which is EASY to use with React-native. It also gives us a nice text string to use in our source format.

{
img1: require('foo.jpg');
}
<Image source=img1 />

This works great if you have a static set of images at app build time — and you only want to change them when building a new app.

Next, we turn to require.context — which is functionality that allows us to specify a glob when using require. In essence, this allows us to specify a directory, and require all files of a certain type in the directory structure. A glob is a wildcard or other greedy specifier, much like you would use on the command line. (Ie — require *.js)

Downside here is:

  • It currently does not seem to work with Babel — but that will change in the future.
  • You don’t know what assets you have mapped in. There’s no nice array, or dictionary of components. So, you’d be able to map them in, but using them gets a lot harder without indirection. This then has code cleanliness issues. This works well with modules, but not so well with assets, as assets need to be referred to later to load, while modules can be used by name.

Otherwise, this is exactly the same as the ‘require’ — static, at app build time.

You may have already guessed it, but as we saw above, we can download a file and use that file as an image.

One way around this limitation is to download an entire bundle\archive of images. This could be easily served from a CDN or other mechanism. (You would, of course, need versioning on your archive here so that you can check for the latest update.)

The bundle could have a JSON description file in it, describing each image, or providing other metadata, including file locations.

Once downloaded, the bundle would be unpacked, and then used as above — loading the Image using the “file://” URI indicator.

This has the advantages of being dynamic, if that is a requirement. The images can be updated on the fly without having to republish an app and get users to update.

It has a disadvantage that the images are not available till the user does an additional step to download the images. For a user on a limited or slow connection, this could be a significant downside — I always like my app to be ready to go when I first install it.

Lastly, we could specify a bundle of assets, built during the build or as a separate step, that contained our assets and JSON. If we can get this bundle transferred to the App, it will allow us to unpack the bundle and use the Assets natively from the JSON.

One method of achieving this would be to use the new Expo “Assets” module. This would specify your archive as an Asset, and then you could unpack that archive locally. All files would be available at the start of your app, with no downloading required. Your app could provide an optional “Check for Updates” sort of functionality to get new Assets.

This combines the download assets with the static asset update methodology.

And that of course, wraps up this rather long article, which was supposed to be pretty short. :)

For my purposes, I will be going the bundle of assets method, which will allow me to keep each logical unit in its own bundle, unpacked when necessary.

Perhaps another article on an investigation into the best method to bundle assets would be necessary.

LINKS

Code:

Metro :

JavaScript:

Image:

Filesystem:

React Native:

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Allan Graves

Written by

Years of technology experience have given me a unique perspective on many things, including parenting, climate change, etc. Or maybe I’m just opinionated.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Allan Graves

Written by

Years of technology experience have given me a unique perspective on many things, including parenting, climate change, etc. Or maybe I’m just opinionated.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store