33

I’m thinking of using Gatsby-Image for my next project and have been playing around with it a little.

I got it to work on my test project, but then I came up with a use case that I would like to use the <Image /> tag from Gatsby much like a regular <img src”image.png”> tag. How can I make the Gatsby <Image /> component reusable?

import React from "react"
import { StaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"
function renderImage({ file }) {
  console.log({ file })
  return <Img fluid={file.childImageSharp.fluid} />
}

// Stateless Image component which I guess will
// receive the 'src' value as a property?
//
// It returns a StaticQuery component with a query property
// and render property. The query property has the GraphQL
// query to receive the images and render prop returns a
// renderImage function which in return, returns an Img
// component from Gatsby with set attributes.
const Image = () => (
  <StaticQuery
    query={graphql`
      query {
        file(relativePath: { eq: "gatsby-astronaut.png" }) {
          childImageSharp {
            fluid(maxWidth: 300) {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    `}
    // render={data => <Img fluid={data.placeholderImage.childImageSharp.fluid} />}
    render={renderImage}
  />
)
export default Image

My optimal use case would be to make a dynamic request to my relativePath which is defined in my Gatsby.config file and then combine the src property in each Gatsby <Image /> and match it with all my images in my assets file and then display it. Is this even possible with <StaticQuery />?

I read in the documentation that StaticQuery can't take variables - only pages. But I don't want my images to be associated with a page - I want to use this component anywhere I want - like a regular img tag.

This is an example: https://codesandbox.io/s/py5n24wk27

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
erikos93
  • 677
  • 2
  • 12
  • 26
  • 1
    Since `gatsby-image` requires pre-processing from gatsby itself, it's not possible to make it truly dynamic at runtime as with an `img` tag. You have to use `StaticQuery` or get the data in the regular page query. – Tholle Mar 12 '19 at 13:42
  • 1
    I've deleted my answer because I just tried it and can confirm that it's not possible currently. There's a technical limitation, Gatsby actually extracts and parses the queries ahead of time and it doesn't evaluate dynamic code as it's doing that. Seems like a pretty concrete technical consideration. Source: https://github.com/gatsbyjs/rfcs/pull/3 – Richard Vanbergen Mar 12 '19 at 14:24
  • Hmm, ok I understand. The search continues I guess! Thanks – erikos93 Mar 12 '19 at 15:00

4 Answers4

46

This may answer your question:

The final code:

import React from 'react';
import { StaticQuery, graphql } from 'gatsby';
import Img from 'gatsby-image';

// Note: You can change "images" to whatever you'd like.

const Image = props => (
  <StaticQuery
    query={graphql`
      query {
        images: allFile {
          edges {
            node {
              relativePath
              name
              childImageSharp {
                fluid(maxWidth: 600) {
                  ...GatsbyImageSharpFluid
                }
              }
            }
          }
        }
      }
    `}
    render={data => {
      const image = data.images.edges.find(n => {
        return n.node.relativePath.includes(props.filename);
      });
      if (!image) {
        return null;
      }

      //const imageSizes = image.node.childImageSharp.sizes; sizes={imageSizes}
      return <Img alt={props.alt} fluid={image.node.childImageSharp.fluid} />;
    }}
  />
);

export default Image;

Using the image:

import Image from '../components/Image';
<div style={{ maxWidth: `300px` }}>
    <Image alt="Gatsby in Space" filename="gatsby-astronaut.png" />
</div>

Explanation

Because StaticQuery doesn't support string interpolation in its template literal, we can't really pass it any props. Instead we will try and handle check for props in the StaticQuery's Render portion.

Caveats

I'm not 100% sure if this affects compiling time since we are scanning all images.

If you have many images, bundle size can get quite large as this solution does scan all images.

Further customization

You can adjust the code to show a placeholder image if no props are passed.

Alternatives

That said, there is another way you could tackle this, but with a bit more work/code.

Sources

  • I modified the code from this article. (Please note that the article was using deprecated code.)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
RodrigoLeon
  • 461
  • 4
  • 6
  • 53
    The gatsby team really missed the ball on this one. Who woulda thought that web developers don't want to make a separate query and component for every single image. /s – Jonathan Dumaine Nov 06 '19 at 23:51
  • 1
    This is nice. Do note that you pay a cost in the javascript bundle with all these images. That's no problem if they're all used. However, if you add 2-3 image components to get different sizes rendered (different options), then you'll pay all that cost in the bundle. -- So this will work well if you can keep the same options for all images AND you use almost all the images in the graphql query. – odinho - Velmont Nov 22 '19 at 15:27
  • I use allFile with relativeDirectory. So i can render just the images in a specific subfolder. allFile(filter: {relativeDirectory: {eq: "team"}}) {...} – freesh Apr 19 '20 at 12:26
  • 1
    `fluid` is also deprecated now. – KZee Apr 17 '21 at 07:17
10

Unfortunately, from what I can gather, the best solution is to write out individual JavaScript files for images.

In RodrigoLeon's approach, it will cause the bundle size to increase dramatically. Especially if, say, you have more than 50 images. Because anytime you use this and loop through all images you create references to them in the component file. So I would not recommend doing it this way.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dawson Walker
  • 109
  • 1
  • 4
4

The site I am building is an E-commerce platform with thousands of images (for all the products). This presented a major issue with using Gatsby which is querying images. For a long time, I had a component that queried all images and matched them up to their respective product (like proposed in this). This is highly inefficient, throwing warnings about the duration of the query.

An alternate to this is the attach the imageFile to the product on the data level, rather than when trying to render.

File src/gatsby-api/create-resolvers/index.js

const resolvers = {
    AWSAppSync_Product: {
        imageFile: {
            type: 'File',
            resolve: async (source, args, context, info) => {
                const node = await context.nodeModel.runQuery({
                    query: {
                        filter: {
                            Key: { eq: source.image1 }
                        }
                    },
                    type: 'S3Object',
                    firstOnly: true
                });

                if (node && node.imageFile) return node.imageFile;
            }
        },
    },
}

module.exports = {
    resolvers
}

File gatsby-node.js

exports.createResolvers = async ({ createResolvers }) => {
    createResolvers(resolvers)
}

File src/components/image/index.js

import React from 'react'
import Img from 'gatsby-image'

export const Image = props => {
  if (props.imageFile && props.imageFile.childImageSharp && props.imageFile.childImageSharp.fluid) {
    return <Img className={props.imgClassName} alt={props.alt} fluid={props.imageFile.childImageSharp.fluid} />;
  }
};

Then use it like:

<Image
  imageFile={product.imageFile}
  alt=""
/>

AWSAppSync_Product is the type of node I am attaching my File to (which can be found in the GraphQL playground on localhost). The resolve will match the Key of the S3Object with image1 (which is a string) on the product. This allows me to directly use the product images without having to run a query inside the image component.

In my opinion, this is a valuable piece of information once you wrap your head around it and it certainly has helped me a lot.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
LpmRaven
  • 83
  • 7
  • You basically did what [Gatsby's sharp plugin already does](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-transformer-sharp/src/create-resolvers.js), but instead of getting your images on the File type, you get it on your own type. This is a legit solution that one should consider, if the data is known when compiling (in your case, Gatsby knows what products you request, so it will prepare all the used images, only). In no way this is a solution if you want to use Gatsby Image for true dynamic data. Your answer is a good reminder, but no solution to the problem. – Martin Braun Jan 26 '21 at 22:34
  • Yes, it's a technique used across gatsby's ecosystem as it's always most effective to have the data attached before build (to improve performance). This obviously does not work for front-end requests, the images and data must be known at build time. Could you explain why this is not a solution to the problem? It's rarely the case to not know the image data at build time. You can avoid having a component that fetches the data, by requesting the data earlier. – LpmRaven Jan 27 '21 at 03:51
  • Well, great that we agree on the first part. However, I wasn't saying that your solution is no solution to the problem. I said it is no solution when you want to handle "true dynamic data". So if you are strict and want to use the Gatsby's `` component "like a regular img tag" (like the question states), this won't work. It's still not bad for people who don't need to show true dynamic images, but as soon as someone implements for instance a "load more products" AJAX call that involves showing the images of the added products using Gatsby Image, it cannot work. – Martin Braun Jan 28 '21 at 00:41
  • 1
    Ah yes, this totally bypasses a front-end component solution and would not support a client-side call for data; all data would be needed at build time. Thanks for the clarification. – LpmRaven Jan 31 '21 at 15:09
0

If you use WordPress with WP GraphQL and want to load some posts, dynamically, you will face the same issue.

You will be unable to use the great pre-processing features, like lower the quality and use rough Base64 placeholders. As many before mentioned, RodrigoLeon's solution will work, but you will face a huge load if your site grows in images, eventually.

Since my website will feature a lot of posts and will feature dynamic loading such posts, I had to come up with a solution that is, at least, acceptable. I end up generating the childImageSharp (and providing a generic Base64 placeholder) for the dynamic part of my website, so I can always pass childImageSharp to the <Img> component of Gatsby.

Here is an example for the fluid image type for featured images on posts:

  • Make sure to include this in your GraphQL:
featuredImage {
  node {
    sourceUrl
    mediaDetails {
      file
      width
      height
      sizes {
        file
        name
        width
        sourceUrl
      }
    }
  }
}

After loading your posts, send each node of your File nodes (featuredImage) through this function:

/**
 * Attaches a sharped image to the node for Gatsby Image.
 * @param image Dynamic image node to expand.
 * @param maxWidth Real existing width of file to use as default size.
 */
function attachChildImageSharp(
  image,
  maxWidth
) {
  const mediaDetails: IWpMediaDetails = image.mediaDetails;
  if (mediaDetails) {
    maxWidth = maxWidth || mediaDetails.width;
    image.localFile = image.localFile || {};
    image.localFile.childImageSharp = image.localFile.childImageSharp || {};
    const childImageSharp = image.localFile.childImageSharp;

    // only supporting fluid right now:
    const fluid = (childImageSharp.fluid =
      childImageSharp.fluid || {});
    fluid.aspectRatio =
      mediaDetails.width && mediaDetails.height
        ? mediaDetails.width / mediaDetails.height
        : undefined;
    fluid.originalImg = image.sourceUrl;
    fluid.originalName = mediaDetails.file;
    fluid.presentationHeight =
      fluid.aspectRatio && maxWidth
        ? Math.round(maxWidth / fluid.aspectRatio)
        : mediaDetails.height;
    fluid.presentationWidth = maxWidth;
    fluid.sizes = `(max-width: ${maxWidth}px) 100vw, ${maxWidth}px`;

    const srcSets = [];
    const allowedSizes = ["medium", "medium_large", "large"];
    mediaDetails.sizes.forEach((size) => {
      if (allowedSizes.indexOf(size.name) >= 0) {
        if (
          size.width === `${fluid.presentationWidth}`
        ) {
          fluid.src = size.sourceUrl;
        }
        srcSets.push(`${size.sourceUrl} ${size.width}w`);
      }
    });

    fluid.srcSet = srcSets.join(",\n");
  } else {
    console.warn("Unable to attach dynamic image sharp: Missing mediaDetails.");
  }
}

You will call the function like this (it is also a good spot to attach your generic Base64 image):

posts.nodes.forEach((post) => {
  attachChildImageSharp(post.featuredImage.node, 768);
   post.featuredImage.node.localFile.childImageSharp.fluid.base64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2NgYGD4DwABBAEAcCBlCwAAAABJRU5ErkJggg=="; // 1x1 black PNG, from https://shoonia.github.io/1x1/#000000ff
});

Beware that an immutable object will cause errors, so disable caching (fetchPolicy: 'no-cache', in case you use the ApolloClient). Pick a width as the second argument that is one of the three widths you selected in the settings of WordPress for image sizes (not including Thumbnail).

This solution is for Gatsby Image V1 and by all attempts, it's not perfect, but it serves my need.

Martin Braun
  • 10,906
  • 9
  • 64
  • 105