survivejs / webpack-book

From apprentice to master (CC BY-NC-ND)
https://survivejs.com/webpack/
2.42k stars 319 forks source link

Loading Images - example for creating img elements #80

Closed wichert closed 7 years ago

wichert commented 8 years ago

I find myself needing to create img elements dynamically. Something like

let img_src = require(`../../images/icons/effect-${effect}.png`);
return (
    <img src={img_src}/>
);

The image loading chapter does not tell you how to do that; it only gives an example for CSS. I naively tried using the same approach by passing a path relative from the .jsx file to the image to require, but that results in a Error: Cannot find module error. Since I suspect this is a common issue it would be nice to add an example for this kind of use case to the chapter.

bebraw commented 8 years ago

That's a good question. The problem is that webpack relies on static analysis. It needs to be able to deduce what to generate without running the code.

There's a potential way you could try, though. Look up require.context. It's covered briefly in one of the end chapters (chunk one). I believe you might be able to resolve your problem through it.

Let me know how it goes. I'll add a tip related to this issue in the book.

wichert commented 8 years ago

What I ended up doing is this: I created a directory called images at the top level, with all my images in there. In a component (located in app/components/) I do this:

import React from 'react';
var images = require.context('../../images', true);

class MyPage extends React.Component {
    render() {
        let img_src = images(`./icons/effect-${this.state.effect}.jpg`)
        return (
            <img src={img_src} alt=""/>
        );
    }
}

A couple of things to take note:

I am not sure if it is better to create the require context in each component, or somewhere globally and include it as a singleton.

bebraw commented 8 years ago

Looking good. The only gotcha is that it could be difficult to test this particular component due to that require.context dependency, but if you are fine with that, this will work.

if you use the URL loader this will can include base64 blocks in your bundle, so you may want to create a separate bundle for those (I have no idea how to do that one so far).

What sort of output do you want? I think file-loader should help here. You can configure url-loader limit to use file-loader instead given file size goes beyond the given value.

I am not sure if it is better to create the require context in each component, or somewhere globally and include it as a singleton.

It probably depends on how you intend to use the images. In any case hiding require.context behind a module interface could be a good move as then you decouple your code for webpack at least a little bit. The idea is that you could replace the mechanism behind the interface later without having to change a lot of code.

wichert commented 8 years ago

I would expect a test to pull in the requirement as well, so how will that make testing more difficult?

Normally your images (and other assets) do not change as much as your code, so for the same reason you create a separate vendor bundle it probably makes sense to create a separate asset-bundle with often used assets.

bebraw commented 8 years ago

I would expect a test to pull in the requirement as well, so how will that make testing more difficult?

I mean testing outside of webpack environment (shallow testing through React with Mocha and so on).

Normally your images (and other assets) do not change as much as your code, so for the same reason you create a separate vendor bundle it probably makes sense to create a separate asset-bundle with often used assets.

Yeah, that makes sense. You can probably generate a bundle like that through the ExtractTextPlugin (same idea as for CSS).

gotbahn commented 8 years ago

@wichert @bebraw awesome solution, thx. I was just looking for something like that to cachebust dynamic images. Worth to be included in book.

bebraw commented 7 years ago

I have expanded the chunk portion somewhat so it's probably safe to close this one.

Feel free to open more specific issues if you think there's something missing.

Max-im commented 5 years ago

You should think to share static folder on backend, save your images there and reach them on http://localhost:/

chrisheseltine commented 4 years ago

FYI this causes issues with Jest as the function is webpack provided.

  ● Test suite failed to run

    TypeError: require.context is not a function

      21 | import iconSavePNG from 'img/icon-save.png';
      22 |
    > 23 | const images = require.context('img', true);
         |                        ^
      24 |
      25 | export default [
      26 |   {

      at Object.<anonymous> (src/content/structure.js:23:24)
      at Object.<anonymous> (src/components/ContentExplorer.js:11:1)
      at Object.<anonymous> (src/views/App.js:6:1)
      at Object.<anonymous> (src/views/App.test.js:4:1)

Code works but can't shallow render the component for tests, Jest throws a hissy.

Any solutions welcomed!

bebraw commented 4 years ago

@chrisheseltine I think I would extract the const images require.context('img', true) call to a module of its own to import and then mock it to return something that works with your tests. Here's an example: https://stackoverflow.com/a/42439030/228885 .

chrisheseltine commented 4 years ago

I tried this, the images worked still, but the test still errored with the mock. In the end I reverted back to referencing files as strings and passing them directly into tags, took a little rewriting is all.

aseem2625 commented 3 years ago

I tried this https://github.com/survivejs/webpack-book/issues/80#issuecomment-216068406

images(imagePath).default

I had to add .default to make it work.

Other solution for those if this doesn't work,

const Image = ({ name, className, ...restProps }) => {
  const src = require(`assets/images/${name}`).default;

  name = name.replace('/', '-'); // For name provided as path, replace '/' with '-' for class names, otherwise / doesn't work in css of that class.

  return (
    <img
      {...restProps} 
      src={src}
    />
  );
};

Strangely, for me, this solution works in one of the repos but not in exactly same repo(probably some minor version change in package-lock.json). There it throws Critical dependency is an expression in the build.

smvora4u commented 3 years ago

I tried this #80 (comment)

images(imagePath).default

I had to add .default to make it work.

Other solution for those if this doesn't work,

const Image = ({ name, className, ...restProps }) => {
  const src = require(`assets/images/${name}`).default;

  name = name.replace('/', '-'); // For name provided as path, replace '/' with '-' for class names, otherwise / doesn't work in css of that class.

  return (
    <img
      {...restProps} 
      src={src}
    />
  );
};

Strangely, for me, this solution works in one of the repos but not in exactly same repo(probably some minor version change in package-lock.json). There it throws Critical dependency is an expression in the build.

how can we load images without ".default" at end? I mean with require("path") only should work.

bebraw commented 3 years ago

@smvora4u You could use named exports. I.e.

const someAsset = '...';

export { someAsset };

Then you can do const src = require(assets/images/${name}).someAsset.

The .default bit comes from the way default exports work (unfortunate legacy restriction).