Search code examples
javascriptreactjsbackground-imagees6-modulesgatsby

React / Gatsby dynamic background images in a component


I've got a React component that essentially does related content, showing a user the next / prev pieces of content inside of a site.

This component needs to import background images so I can use them as inline styles inside of the component. However, to know which background image I want to import, I have to look at the props which get defined like this:

<LocationRelated 
  previousLocationName="Columbus, OH"
  previousLocationPath="columbus"
  nextLocationName="St. Pete Beach, FL"
  nextLocationPath="st-pete-beach"
/>

But, as far as I can tell, there isn't a way to look at the props of a component before it enters React's render function - thus no way to do a dynamic import.

This is my broken attempt at doing this, using an ES6 template literal inside of the import:

import React from "react"
import pfbRelatedPrevBGImagePath from '`../../images/grid/PFB_${this.props.previousLocationPath}.jpg`'
import pfbRelatedNextBGImagePath from '`../../images/grid/PFB_${this.props.nextLocationPath}.jpg`'

class LocationRelated extends React.Component {

  render() {

    const pfbRelatedPrevBG = { 
      backgroundImage: `linear-gradient( to bottom, rgba(1,1,1,0.2), rgba(2,2,2,0.2) ), url(${ pfbRelatedPrevBGImagePath })` 
    }
    const pfbRelatedNextBG = { 
      backgroundImage: `linear-gradient( to bottom, rgba(1,1,1,0.2), rgba(2,2,2,0.2) ), url(${ pfbRelatedNextBGImagePath })` 
    }

    return (
      <div>
        <aside className="pfb-longform-container">
          <section className="pfb-related-content">
            <h3 className="pfb-related-content-title">Additional Locations</h3>
            <section className="pfb-related-image-container">
              <div 
                className="pfb-related-1" 
                style= { pfbRelatedPrevBG }
              >
                <p className="pfb-related-text">{ this.props.previousLocationName }</p>
              </div>
              <div 
                className="pfb-related-2"
                style= { pfbRelatedNextBG }
              >
                <p className="pfb-related-text">{ this.props.nextLocationName }</p>
              </div>
            </section>
          </section>
        </aside>
      </div>
    )
  }
}

export default LocationRelated

My webpack builder errors out, saying basically it can't read this.props.whatever because it hasn't been defined yet when it tries to read the import. Is there a way to do this?

Additional note: I'm using the Gatsby static site generator for this site, but that shouldn't really impact what's happening (at least I don't think it should).


Solution

  • The above approach is impossible as best I can tell, but as I discovered, it's impossible for a good reason. You have to FULLY give into the React (Gatsby) way of doing things and my approach above was trying to do this from a Drupal/WordPress-CMS-build perspective.

    Check out my revised component for LocationRelated:

    import React from "react"
    import Link from 'gatsby-link'
    
    class LocationRelated extends React.Component {
    
      render() {
    
        // Setup inline style objects using ES6 template literals which pull in the correct paths from the content pages themselves
        const pfbRelatedPrevBG = { 
          backgroundImage: `linear-gradient( to bottom, rgba(1,1,1,0.2), rgba(2,2,2,0.2) ), url(${ this.props.prevLocationImgPath.sizes.src })` 
        }
        const pfbRelatedNextBG = { 
          backgroundImage: `linear-gradient( to bottom, rgba(1,1,1,0.2), rgba(2,2,2,0.2) ), url(${ this.props.nextLocationImgPath.sizes.src })` 
        }
    
        return (
          <div className="pfb-related-bg-container">
            <aside className="pfb-related-container">
              <section className="pfb-related-content">
                <section className="pfb-related-image-container">
                  <Link 
                    to={ this.props.prevLocationPath }
                    className="pfb-related-left-arrow"
                  >
                    <img src= { LeftArrow } alt="Navigate to the previous location in the PFB Annual Report" />
                    <div className="pfb-related-left-arrow-text">Previous</div>
                  </Link>
                  <Link 
                    to={ this.props.prevLocationPath }
                    className="pfb-related-1" 
                    style={ pfbRelatedPrevBG }
                  >
                    <p className="pfb-related-text">{ this.props.prevLocationName }</p>
                  </Link>
                  <Link 
                    to={ this.props.nextLocationPath }
                    className="pfb-related-2"
                    style= { pfbRelatedNextBG }
                  >
                    <p className="pfb-related-text">{ this.props.nextLocationName }</p>
                  </Link>
                  <Link 
                    to={ this.props.nextLocationPath }
                    className="pfb-related-right-arrow"
                  >
                    <img src= { RightArrow } alt="Navigate to the next location in the PFB Annual Report" />
                    <div className="pfb-related-right-arrow-text">Next</div>
                  </Link>
                </section>
              </section>
            </aside>
          </div>
        )
      }
    }
    
    export default LocationRelated
    

    You'll notice not much is different than the above attempt but pay attention to the this.props.nextLocationImgPath.sizes.src path being used in the backgroundImage object literal. You have to pass backgroundImage the source path as a prop. Don't try to do it in the component like I was. React's separation of concerns is about getting data out of components and instead feeding data in through props.

    So then the question becomes: if you have to pass the path in as a prop, how do you get the path when you call your component at the page level?

    If you're using Gatsby (and you should), you need to use GraphQL to get the optimized image paths. In my example, we will call for background-image path on the same page where we would call the LocationRelated component.

    Simplified example of my page that concentrates on the issue at hand:

    import React from 'react'
    import Img from "gatsby-image";
    
    import LocationRelated from '../components/LocationRelated'
    
    class CharlotteLocation extends React.Component {
      render() {
        return (
          <div>    
            <LocationRelated 
              prevLocationName="Detroit"
              prevLocationPath="detroit"
              prevLocationImgPath= { this.props.data.charlottePrevImage }
              nextLocationName="Montana"
              nextLocationPath="montana"
              nextLocationImgPath= { this.props.data.charlotteNextImage }
            />
          </div>
        )
      }
    }
    
    export default CharlotteLocation 
    
    // GraphQL queries for each image
    export const CharlotteImageQuery = graphql`
      query CharlotteImageQuery {
        charlottePrevImage: imageSharp(id: { regex: "/PFB_DETROIT/" }) {
          sizes(maxWidth: 600 ) {
            ...GatsbyImageSharpSizes_withWebp
          }
        },
        charlotteNextImage: imageSharp(id: { regex: "/PFB_MONTANA/" }) {
          sizes(maxWidth: 600 ) {
            ...GatsbyImageSharpSizes_withWebp
          }
        }
      }
    `
    

    These GraphQL queries make available the optimized paths to the files we need, and now we can send them into our LocationRelated component via this.props.data.

    gatsby-image and GraphQL are a bit tricky to work with at first so check out this nice tutorial by Kyle Gill. It got me going a bit better than the Gatsby documentation did.

    For reference, here is how I set up my gatsby-config.js in the project to get all these plugins working nicely together (I'm pulling the images from a standalone images directory in my project rather than in a component, so I needed gatsby-source-filesystem to make all this work along with gatsby-image, gatsby-transformer-sharp, gatsby-plugin-sharp,):

    module.exports = {
      siteMetadata: {
        title: 'PFB2017 Site',
      },
      //pathPrefix: "/pfb2017-site",
      plugins: [
        // Adds in React Helmet for metadata
        `gatsby-plugin-react-helmet`,
    
        // Gives us sass in the project
        `gatsby-plugin-sass`,
    
        // This plugin transforms JSON, which is how we are storing our location data
        `gatsby-transformer-json`,
    
        // Adds in Gatsby image handling
        `gatsby-image`,
        `gatsby-transformer-sharp`,
        `gatsby-plugin-sharp`,
    
        /** 
         * This is how you customize gatsby-source-filesystem
         * Check this page out for more background 
         * https://www.gatsbyjs.org/docs/building-with-components 
         * By default, the gatsby-default starter kit comes with the `pages` directory wired up
         * I'm adding it here for consistency but you don't need it
         * What I have added into the starter are `images` and `data`, subfolders we'll need for the PFB project
         * 
         */
    
        {
          resolve: `gatsby-source-filesystem`,
          options: {
            name: `pages`,
            path: `${__dirname}/src/pages`,
          },
        },    
        {
          resolve: `gatsby-source-filesystem`,
          options: {
            name: `data`,
            path: `${__dirname}/src/data`,
          },
        },
        {
          resolve: `gatsby-source-filesystem`,
          options: {
            name: `images`,
            path: `${__dirname}/src/images`
          }
        }
      ],
    };