7
\$\begingroup\$

I've been doing some research around how to best make use of the WebP images (with fallback) in React. Most solutions point to something similar to what I've written below. That means if I have a lot of images I have to export the same image twice in assets.js (both PNG and WebP), import twice and provide two Image props for the same image.

This feels very inefficient. Is this an uncommon way of working with images? Is there a better way for working with images that live locally?

assets.js - where I collect all my assets and export them as strings. Imagine this list being really lengthy.

export { default as image1 } from "./assets/image-1.png";
export { default as image1Webp } from "./assets/image-1.webp";

In the file I import the assets I need for that page

import {
    image1,
    image1Webp
} from "./assets";

The component + props

<Image
  src={image1Webp}
  fallback={image1}
  alt="Lorem"
/>

Image.js component

const Image = ({
  src,
  fallback,
  type = 'image/webp',
  ...delegated
}) => {
  return (
    <picture>
      <source srcSet={src} type={type} />
      <img src={fallback} {...delegated} />
    </picture>
  );
};

Some things I've tried:

Generally the recommended way is to add images to a React project is to use require(img/path). So I tried things like appending .png or .webp to the base URL, inside the Image component. so I only need to define one string and then the component handles the splitting.

e.g. pseudocode:

export const toWEBP = (src) => {
  const location = src.split(".")
  return `${location}.webp`
}
\$\endgroup\$
2
  • \$\begingroup\$ You aren’t using webpack or a similar server-side tool to transpile/package the code are you? And do all image file names have a similar pattern, such that the names could be put into an array, e.g. [“image-1”, “image-2”...]? \$\endgroup\$ Commented Jul 3, 2020 at 12:32
  • 1
    \$\begingroup\$ I'm using create-react-app as a starting point. And as far as I'm concerned they don't expose webpack settings \$\endgroup\$
    – umbriel
    Commented Jul 3, 2020 at 12:42

1 Answer 1

3
+50
\$\begingroup\$

It would be great if the code could be simplified using dynamic imports. I haven't tried this myself but if the image names could be put into an array then something like the following might work, where the exported images are set as properties on an object that can be exported. When this SO answer to Create a loop to import files dynamically in React was written only Chrome and Safari supported that feature but since then it appears that Edge and Firefox now offer support as well.

const images = {};
const nums = [1, 2];
for (const format of  ['png', 'webp']) {
  for (const num of nums) {
    const key = `image${num}${format === 'webp' ? 'webp' : ''}`;
    images[key] =  await import( `./assets/image-${num}.${format}`);
  }
}

But according to the Hacks blog post by Jason Orendorff ES6 In Depth: Modules

For a dynamic language, JavaScript has gotten itself a surprisingly static module system.

  • All flavors of import and export are allowed only at toplevel in a module. There are no conditional imports or exports, and you can’t use import in function scope.
  • All exported identifiers must be explicitly exported by name in the source code. You can’t programmatically loop through an array and export a bunch of names in a data-driven way.

So the function code above could be exported in an async function:

export async function getImages() {
    const images = {};
    //...set keys on images
    return images;
}

Then that function can be imported:

import { getImages } from './assets.js';

But to use that function it would need to be run in an async function:

(async () => {
  const images = await getImages();
  //use images
})();

Other approaches are listed in answers to this similar post on freecodecamp.org - e.g. from this answer by Dan Couper:

One pretty simple solution:

// images.js
const images = [
  { id: 1, src: './assets/image01.jpg', title: 'foo', description: 'bar' },
  { id: 2, src: './assets/image02.jpg', title: 'foo', description: 'bar' },
  { id: 3, src: './assets/image03.jpg', title: 'foo', description: 'bar' },
  { id: 4, src: './assets/image04.jpg', title: 'foo', description: 'bar' },
  { id: 5, src: './assets/image05.jpg', title: 'foo', description: 'bar' },
  ...etc
];
export default images;
// MyComponent.js
import images from './images'
//...snip
{ images.map(({id, src, title, description}) => <img key={id} src={src} title={title} alt={description} />)

You’re basically writing a database of images that match the ones in assets.

Then you can be clever in images.js to pull the correct prefix based on the environment if you want. There will be a WebPack plugin that kinda does this as well if you have a google around (you want it to take a directory of files and generate an iterable set of paths to them).

The import as described is more for importing one-off images (logos etc) rather than importing lots of images from a directory.


Regarding the pseudocode at the end of the post:

export const toWEBP = (src) => {
  const location = src.split(".")
  return `${location}.webp`
}

if src contained an extension (e.g. png) then webp would be appended to that existing extension. One option would be to use pop to remove the existing extension and also push webp onto the array before using it in the return value.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks Sam for looking into this. I love all these suggestions. I was having trouble getting the async/await stuff to work in React, even though it seems like the solution is mostly aimed towards a fixed name pattern e.g. image-1.png, image-2.png etc. What if the images have all different names? And what if the images are more sporadically placed (like an article) and not mapped in map. Mapping/looping all the images in one go is fairly trivial. It’s when you want to load the images at will, thats when I start to struggle to find a dry solution. \$\endgroup\$
    – umbriel
    Commented Jul 5, 2020 at 16:59
  • \$\begingroup\$ Even if the images have different names, would those be consistent between the formats, e.g. code.png and code.webp? There is another answer by Dan in that that thread that might offer inspiration about async loading... \$\endgroup\$ Commented Jul 5, 2020 at 17:11
  • \$\begingroup\$ Yes the name would still be consistent between the formats. I got the async working, Now I'm getting in the console.log: default: "/static/media/monomesh-red2.4b122a66.webp". Now I've got to figure out how it can work with different name patterns. \$\endgroup\$
    – umbriel
    Commented Jul 5, 2020 at 17:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.