Published on

Loading UX w/ Relay Fragments

Authors

📓 History

We've all seen The Redux Container Pattern. This pattern is used to ensure you can make sure you can have a reusable component (the presentational component), and a component which fully describes its own data requirements (the container).

const ReUsableComponent = ({ person: { name, age, gender } }) => {
  return (
    <div>
      <span>Name: {name}</span>
      <span>Age: {age}</span>
      <span>Gender: {gender}</span>
    </div>
  )
}

const ContainerComponent = ({ apiResponse }) => {
  // From a given API response, how do I show the person?
  const people = getPeopleFromApiRespnse(apiResponse)

  return (
    <ul>
      {people.map((person) => (
        <ReUsableComponent person={person} />
      ))}
    </ul>
  )
}

⏱ History Repeats

It might not be immediately obvious, but this pattern shows up in Relay too.

Typically when you're writing a feature in Relay, you start with the data you need to show the component. So I usually start my components like this.

const PersonCard = ({ personRef }) => {
  const person = useFragment(
    graphql`
      fragment PersonCardFragment on Person {
        age
        name
        gender
      }
    `,
    personRef
  )

  // some interesting UI...
}

Okay, now that I've finished this cool piece of UI, I need to wire it up to the page's query.

// some-page-component.js

const SomePageComponent = () => {
  const { viewer } = useLazyLoadQuery(
    graphql`
      query SomeComponentQuery {
        viewer {
          shop {
            owner {
              user {
                ...PersonCardFragment
              }
            }
          }
        }
      }
    `,
    {}
  )

  return <ComponentThatEventuallyRendersPersonCard userRef={viewer.shop.owner.user} />
}

Here's what the UI looks like.

Page Layout

😳 The Problem

The problem with the above implementation is now SomePageComponent will suspend causing this horrible UX to occur.

Bad loading experience

When what you really want is this.

Make sure you have granular suspense boundaries

Good loading experience

✅ The Fix

To achieve this, you need to push the data requirements down.

So your PersonCard will look like this

const PersonCard = ({ queryRef }) => {
  const { viewer } = useFragment(
    graphql`
      fragment PersonCardFragment on Query {
        viewer {
          shop {
            owner {
              user {
                age
                name
                gender
              }
            }
          }
        }
      }
    `,
    queryRef
  )

  // some interesting UI...
}

and your SomePageComponent will look like this

// some-page-component.js

const SomePageComponent = () => {
  // This component won't suspend as long as it
  // doesn't have any direct data requirements.
  //
  // Now, PersonCard will suspend instead of this component
  const query = useLazyLoadQuery(
    graphql`
      query SomeComponentQuery {
        ...PersonCardFragment
      }
    `,
    {}
  )

  return <ComponentThatEventuallyRendersPersonCard queryRef={query} />
}

Now SomePageComponent doesn't suspend (thanks Relay)! You can wrap just the PersonCard component in a Suspense component and your UI will look great while the data is loading.

Shared Components

We've solved the horrible loading state UX, but now we have another "problem" 😱. Our PersonCard can't be reused elsewhere since it's tied to the logged in user's shop owner.

To solve this, we need some sort of Container / Presentational components.

// Presentation Component
const PersonCard = ({ personRef }) => {
  const person = useFragment(
    graphql`
      fragment PersonCardFragment on Person {
        age
        name
        gender
      }
    `,
    personRef
  )

  // some interesting UI...
}
// Container Component
const LoggedInShopOwnerPersonCard = ({ queryRef }) => {
  const { viewer } = useFragment(
    graphql`
      fragment LoggedInOwnerFragment on Query {
        viewer {
          shop {
            owner {
              user {
                ...PersonCardFragment
              }
            }
          }
        }
      }
    `,
    queryRef
  )

  return <PersonCard personRef={viewer.shop.owner.user} />
}

Your page component should look roughly the same. Instead of using the PersonCard component / fragment, we'll use the LoggedInShopOwnerPersonCard component / fragment.

// some-page-component.js

const SomePageComponent = () => {
  // This component won't suspend as long as it
  // doesn't have any direct data requirements.
  //
  // Now, PersonCard will suspend instead of this component
  const query = useLazyLoadQuery(
    graphql`
      query SomeComponentQuery {
        ...LoggedInShopOwnerPersonCard
      }
    `,
    {}
  )

  return <ComponentThatEventuallyRendersLoggedInShopOwnerPersonCard queryRef={query} />
}

🔖 In Review

  1. First, make sure you really need to share a component before you go enforce all of your developers to make two components per component.

  2. Pushing your data requirements to the leaves makes loading states a breeze. Take that query ref as far as you possibly can.

  3. Only do this if you're sure the components are the same. Don't try bending your components with 100 props to get it to look correct. You'll end up fetching data you didn't need in the first place.