1
votes

I have a problem with inconsistent behavior between the site generated by gatsby develop and gatsby build. The result is a site that works in development but not production.

A summary of my site:

A simple blog-like site (personal profiles instead of blog posts). The index page is a list of people, and each item in that list links to that person's profile page.

I'm using Gatsby to build the site. My data (the personal profiles) are entries hosted on the Contentful headless CMS. I'm using the gatsby-source-contentful source plugin.

High level description of the problem

I cannot shuffle the order of the profile list items on the index page. The only behavior where my site goes beyond any basic gatsby tutorial is that I want to randomize the list of profiles on my index page (to give everyone a fair chance at being listed at the top).

gatsby build generates a static index page with the list in one permutation. When loaded in a browser the ThumbList component re-shuffles those items to another permutation on render and some sub-elements are not properly managed by react and stay stuck as other elements shift. This leads, for example, to profile images paired with the wrong name.

The code

The following code is somewhat summarized for readability.

src/pages/index.js:

import React from "react"
import Layout from "../components/layout"
import ThumbList from "../components/thumbList"
import { graphql } from "gatsby"

export default ({data}) => {
  // people are called "creators" in the app
  const creatorData = data.allContentfulCreator.edges
  const shuffledData = shuffle(creatorData.slice(0))
    return (
      <Layout>
        <ThumbList data={shuffledData} />
      </Layout>
    )
}

const shuffle = (a) => {
  // Fisher-Yates randomized array in-place shuffle algo
  // ...
  return a
}

export const query = graphql`
  {
    allContentfulCreator {
      edges {
        node {
          id
          slug
          name
          bio {
            id
            bio
          }
          mainImage {
            file {
              url
            }
          }
        }
      }
    }
  }
`

src/components/thumbList.js:

import React from "react"
import { Link } from "gatsby"

// A list of creator profile links, with name and picture thumbnail
export default ({data}) => {
  return (
    <div>
      <ul>
        {
          data.map(({node}) => {
            const creator = node
            const link = "/" + creator.slug
            const image = "https:" + creator.mainImage.file.url

            return (
              <li key={creator.id}>
                <Link to={link}>
                  {creator.name}
                </Link>
                <img src={image} />
              </li>
            )
          })
        }
      </ul>
    </div>
  )
}

The result of gatsby build is an index.html containing:

  <ul>
    <li>
      <a href="/alice">
        Alice
      </a>
      <img src="cdn.com/alice.jpg">
    </li>
    <li>
      <a href="/bob">
        Bob
      </a>
      <img src="cdn.com/bob.jpg">
    </li>
    <li>
      <a href="/eve">
        Eve
      </a>
      <img src="cdn.com/eve.jpg">
    </li>
  </ul>

However, when viewing the index page in the browser (via gatsby serve or a deployed version of the site) the live react ThumbList component again shuffles the data in its render method.

The result re-rendered html:

  <ul>
    <li>
      <a href="/alice">
        Bob
      </a>
      <img src="cdn.com/alice.jpg">
    </li>
    <li>
      <a href="/bob">
        Eve
      </a>
      <img src="cdn.com/bob.jpg">
    </li>
    <li>
      <a href="/eve">
        Alice
      </a>
      <img src="cdn.com/eve.jpg">
    </li>
  </ul>

Here only the text nodes are rearranged to match the new order (confirmed by console logging the array order), but the links and image elements remain stuck where they were in the static build. Now the names, images, and links are scrambled.

Two other things to note:

  1. All works fine with gatsby develop. I guess it's because in development index.html is generated without its static content in the body - allowing react complete control over the DOM from the start with no static scaffolding to confuse it.
  2. Using the react inspector I see that the virtual DOM and the real DOM have gotten out of sync. React thinks it has correctly shuffled the list items. Inspector shows something like:

(very abbreviated for readability)

<ul>
    <li key="165e2405">
        <GatsbyLink to="/bob">
            Bob
        </GatsbyLink>
        <img src="cdn.com/bob.jpg"></img>
    </li>
    <li key="067f9afc">
        <GatsbyLink to="/eve">
            Eve
        </GatsbyLink>
        <img src="cdn.com/eve.jpg"></img>
    </li>
    <li key="ca4b82bf">
        <GatsbyLink to="/alice">
            Alice
        </GatsbyLink>
        <img src="cdn.com/alice.jpg"></img>
    </li>
</ul>

My questions

  1. Is this just an un-Gatsby-like approach? This description of a "Hybrid app page" seems to imply that you can either have static or dynamic components. I suppose I'm trying to have it both ways: I want the profiles fetched from contentful via graphql during build so it can be available via static HTML + pre-built json data files (e.g. /static/d/556/path---index-6a9-L7r5Sntxcv3RUIoHYIR3Qqm9Jmg.json), but then I want to dynamically shuffle that data and rearrange the DOM on render. Is that not possible with Gatsby? Do I need to give up the pre-fetched data during build and just consider that a dynamic component and fetch the data via the Contentful API in componentDidMount?
  2. If this approach should be OK, what am I doing wrong?
  3. If this approach is not idomatic, is there a way to modify (shuffle) the data queried via graphql at build time? I'd actually be happier if the data only shuffled at build time and did not re-shuffle at run-time in the browser - I could just automate the site to rebuild every hour or so and the site could be more static to the client.
1
Couple things - what if you did the randomization of the nodes using setTimeout and invoked that function in the React lifecycle method componentWillMount? Just an idea, but that combo could create a timed shuffle of the nodes right before the user loads them, regardless of how they are stored in the database. Yeah, it's client-side but such a small action that I can't imagine much of a perf hit. Another option would be to do a lambda function, do the shuffle server side after the page loads, this would be the hybrid app option. Seems overkill but you could do it that way too.serraosays

1 Answers

1
votes

I've been struggling with this too recently!

My solution is to render the shuffled content once the parent component mounts using ReactDOM's render method:

import React, { useRef, useEffect } from "react";
import { render } from "react-dom";
import shuffle from "../utils/shuffle";

const shuffledArray = shuffle(array.slice());

// The below should still be able to work with graphql fetched data
// as I think the array will be saved to a variable for use in the client,
// although in my case I haven't used it so can't be fully sure
const ShuffledJSXElements = () =>
    shuffledArray.map(creator => (
        <li key={creator.id}>
            <Link to={link}>
                  {creator.name}
            </Link>
            <img src={image} />
      </li>
    ));

const Page = () => {
    const shuffledContentContainerRef = useRef();

    useEffect(() => {
        const ontainer = portfolioContainerRef.current;
        render(<ShuffledJSXElements />, container);
    }, []);

    return (
        <MainWrapper>
            <StyledPortfolioGridWrapper ref={shuffledContentContainerRef} />
        </MainWrapper>
    );
};

export default Portfolio;

One frustrating thing about this is that the container element won't have an awareness of its own height before the content is rendered, so the layout might jump about a bit. A workaround for this is to use a min-height css property.