- Published on
Loading UX w/ Relay Fragments
- Authors
- Name
- Terence Bezman
- @b_ez_man
📓 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.
😳 The Problem
The problem with the above implementation is now SomePageComponent
will suspend causing this horrible UX to occur.
When what you really want is this.
Make sure you have granular suspense boundaries
✅ 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
First, make sure you really need to share a component before you go enforce all of your developers to make two components per component.
Pushing your data requirements to the leaves makes loading states a breeze. Take that query ref as far as you possibly can.
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.