skip to Main Content

I have this React component on my gatsby site:

import React from 'react'
import Layout from '../components/layout'
import SEO from '../components/seo'
import { graphql } from "gatsby"
import Card from '../components/Card'
import Gallery from '../components/Gallery'
import shuffle from 'lodash/shuffle'

const IndexPage = ({data}) => {
  function linkify(string){
    return string.toLowerCase().replace(/["'()]/g,"").replace(/s/g, '-');
  }
  return(
  <Layout>
    <SEO />
    <Gallery>
      {
      shuffle(data.allContentfulAlbum.nodes).map((album) => {
        return (
        <Card src={album.cover.file.url} title={album.title} link={'/album/'+linkify(album.title)} />
        )
        })}
    </Gallery>
  </Layout>
)
    }

export default IndexPage

export const query = graphql`
query Albums {
  allContentfulAlbum {
    nodes {
      cover {
        file {
          url
        }
      }
      title
    }
  }
}
`

Info:

  • This is the index page of a photo gallery.
  • Card is a component that receives and shows the cover photo, the album name in top of the photo and the whole Card is a link to the album page.
  • You can visit this page at https://nicovial.tk so you get the idea, card shuffle is disabled for now.
  • lodash is a JS library with many array processing functions available at npm.

Expected behavior:

  • Card order shuffled on every page refresh or revisit.

Actual behavior:

  • Cover photos and links are NOT shuffled, they’re shown always in the original order.
  • However, album titles are shuffled, so cards are shown with wrong names (except for coincidences on shuffle) but with right links, and in original order always.

The strange detail:

This only happens on production builds (either local build&serve or online github/netlify builds). When running ‘gatsby develop’ the behavior is the expected one.

2

Answers


  1. Chosen as BEST ANSWER

    As @derek-nguyen pointed, I had to do the shuffle on runtime (using useLayoutEffect), storing the shuffled array in the component's state.

    Something important to point out is that using lodash will NOT work, as the 'shuffler' function needs to be declared in the component so it's available at runtime.

    Also, the shuffler function can not update the original array, it will because issues, it needs to return a new one. Example:

    This function will NOT work:

      function shuffleArray(array) {
      for (let i = array.length - 1; i > 0; i--) {
          const j = Math.floor(Math.random() * (i + 1));
          [array[i], array[j]] = [array[j], array[i]];
      }
      }
    

    This one will work well:

    function shuffleArray(array) {
      let tempArray = array;
      for (let i = tempArray.length - 1; i > 0; i--) {
          const j = Math.floor(Math.random() * (i + 1));
          [tempArray[i], tempArray[j]] = [tempArray[j], tempArray[i]];
      }
      return tempArray;
    }
    

    So this is the updated component, now working on production builds:

    import React, { useLayoutEffect, useEffect, useState } from 'react'
    import Layout from '../components/layout'
    import SEO from '../components/seo'
    import { graphql } from "gatsby"
    import Card from '../components/Card'
    import Gallery from '../components/Gallery'
    
    function linkify(string){
      return string.toLowerCase().replace(/["'()]/g,"").replace(/s/g, '-');
    }
    function shuffleArray(array) {
      let tempArray = array;
      for (let i = array.length - 1; i > 0; i--) {
          const j = Math.floor(Math.random() * (i + 1));
          [array[i], array[j]] = [array[j], array[i]];
      }
      return tempArray;
    }
    const IndexPage = ({data}) => {
      const [shuffled,setShuffled] = useState([]);
      useLayoutEffect(() => {
        setShuffled(shuffleArray(data.allContentfulAlbum.nodes));
      })
      return(
      <Layout>
        <SEO />
        <Gallery>
          {
          shuffled.map((album, index) => {
            return (
            <Card src={album.cover.file.url} title={album.title} link={'/album/'+linkify(album.title)} key={index} />
            )
            })}
        </Gallery>
      </Layout>
    )
        }
    
    export default IndexPage
    
    export const query = graphql`
    query Albums {
      allContentfulAlbum {
        nodes {
          cover {
            file {
              url
            }
          }
          title
        }
      }
    }
    `
    

  2. 2 things that might help:

    • Your Card list is missing the key props, which is important for React to keep your list in order when it’s shuffled.

    • In Gatsby production, your html is already pre-rendered. This means when React tries to rehydrate the dom, it will find that the new random list doesn’t match the server-rendered list.

    You can get around this by storing the index order in component state, then shuffle it in a dependency-less useEffect or componentDidMount. The drawback is that there will be a flash of content change, which can be managed via CSS, but SEO has to be taken into account.

    If SEO doesn’t matter, you can simply render nothing until the order has been shuffled.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search