A Fragment Container is a higher-order component that allows components to specify their data requirements. A container does not directly fetch data, but instead declares a specification of the data needed for rendering, and then Relay will guarantee that this data is available before rendering occurs.
Source: Relay docs: Fragment Container
Building a React app requires intentional effort to isolate components. Markup, styles, state management, etc. typically belong to the nearest component that needs them.
Relay allows us to also precisely define the data required to render each component in a tree, with Fragment Containers.
If a QueryRenderer is the top-level component of a tree that renders data, Fragment Containers define the contract each child component needs from the data. While there is a single QueryRenderer associated with each network request in our app, each request will retrieve the data for many Fragment Containers. Imagine the QueryRenderer as the tree and the Fragment Container as its branches.
🎯 In this exercise we'll convert several small, isolated components into Fragment Containers, and specify the data each component requires.
Start the app:
💻 Run yarn start-exercises
from a console pointed at the root of this project
View the app for this exercise in a browser:
💻 Visit localhost:1234/exercise-2
This app looks pretty similar to the app in Exercise 1. It renders a list of artists:
When you click on an artist's name, it takes you to a detail page for that artist. The artist detail page shows the artist name and birth year, bio, and some auction result data:
We're going to focus on the artist detail page for this exercise.
The top-level component responsible for rendering the artist detail page is the Artist2QueryRenderer
component. It should look pretty familiar if you worked through the previous exercise.
When the Artist2QueryRenderer
gets back data from the GraphQL server, it renders an Artist2
component:
export const Artist2QueryRenderer = () => {
...
return (
<QueryRenderer<Artist2QueryRendererQuery>
...
render={({ props }) => {
...
return <Artist2 artist={props.artist} />;
}}
/>
);
The Artist2
component lays out the different sections of our artist detail page: a heading section, a bio section, and a section for auction results.
It passes an artist prop through to each of the child components.
export const Artist2: React.FC<Artist2Props> = ({ artist }) => {
return (
<div>
<Artist2Heading artist={artist} />
<hr />
<Artist2Bio artist={artist} />
<hr />
<Artist2AuctionResults artist={artist} />
</div>
)
}
Each of these section components emits some data from the artist
prop passed in.
This style of component isolation is a common and powerful way to build a React app. Each component includes all the markup, styles, and interaction logic it needs. This is great for reusability, and even greater for comprehensibility.
One thing is missing from each of these components though — a specification of the data it needs to render properly. That's currently handled entirely in the Artist2QueryRenderer
, where all fields for all components are aggregated:
query={graphql`
query Artist2QueryRendererQuery($artistID: ID!) {
artist(id: $artistID) {
name
birthYear
bio
auctionRecord
auctionLotsSoldAnnually
}
}
`}
This works, but it's an anti-pattern because it binds the child components data needs to its parent or grandparents. This not only gets in the way of making our components reusable, but also can make refactoring or scaling components tricky when we have to go a few levels up to find where data is coming from. It'd be great if we could specify these fields in the components that needed them. Then each component would tell a more comprehensive story about what's needed to render it.
Relay offers us Fragment Containers for exactly this purpose. Let's convert these child components to be Fragment Containers!
We'll start by converting the component that combines all our sections together, the Artist2
component.
💻 Add an import statement to Artist2
for the react-relay
dependencies we'll need:
import { createFragmentContainer, graphql } from "react-relay"
./Artist2.tsx
createFragmentContainer
is a higher-order component (HOC), or function that generates a new component based on another component. We'll use it to create a Fragment Container from our Artist2
display component.
graphql
helps us specify the GraphQL fragment associated with this component.
💻 Define and export a new Fragment Container:
export const Artist2FragmentContainer = createFragmentContainer(Artist2, {
artist: graphql`
fragment Artist2_artist on Artist {
name
birthYear
bio
auctionRecord
auctionLotsSoldAnnually
}
`,
})
./Artist2.tsx
The first argument to createFragmentContainer
is the display component you want to turn into a Fragment Container.
The second argument is an object containing properties for each root-level field your component needs to query. In this case, our GraphQL specifies fragment Artist2_artist on Artist
, which indicates we need these fields from the Artist
type of our GraphQL endpoint. The results of this query fragment will be passed to our Artist2
component as a prop named artist
.
We learned in the previous exercise that Relay is particular about the names you give queries associated with QueryRenderers — it's also very particular about the names of fragments you specify for Fragment Containers.
💻 Try changing the name of the GraphQL fragment from Artist2_artist
to Artist2_artisttttt
and you'll get an error in your console:
[relay] Parse error: Error: RelayFindGraphQLTags: Container fragment names must be `<ModuleName>_<propName>`. Got `Artist2_artist`, expected `Artist2_artist`. in "exercises/02-Fragment-Container/Artist2.tsx"
This is another easy mistake to make.
💻 Change Artist2_artisttttt
back to Artist2_artist
.
💻 Replace the Artist2Props
interface with one generated by Relay:
import { Artist2_artist } from "./__generated__/Artist2_artist.graphql"
// ...
interface Artist2Props {
artist: Artist2_artist
}
./Artist2.tsx
When Relay compiled our Fragment Container, it generated this type for us. It might not look like much in this example, but this type-generation saves us from countless typos.
💻 Replace the Artist2
component with the new Artist2FragmentContainer
in Artist2QueryRenderer
:
import { Artist2FragmentContainer } from './Artist2';
// ...
render={({ props }) => {
if (!props || !props.artist) {
return <div>Loading</div>;
}
return <Artist2FragmentContainer artist={props.artist} />;
}}
// ...
./Artist2QueryRenderer.tsx
At this point, there are no visible changes in the browser...but there's now an Artist2FragmentContainer
being rendered by our Artist2QueryRenderer
!
Our final step for this conversion is to remove the individual fields specified in our Artist2QueryRendererQuery
query.
💻 Replace the individual fields specified in the Artist2QueryRendererQuery
query with the Artist2_artist
query fragment:
query={graphql`
query Artist2QueryRendererQuery($artistID: ID!) {
artist(id: $artistID) {
...Artist2_artist
}
}
`}
./Artist2QueryRenderer.tsx
A few things to point out:
-
The
...
beforeArtist2_artist
is how we include a fragment in a GraphQL query. Conceptually it's similar to the spread operator in JavaScript — as if to say, "replace everything here with whatever is defined in the fragment." -
The fragment name (
Artist2_artist
) must match exactly the name defined in ourArtist2FragmentContainer
. -
We don't need to import
Artist2_artist
at the top of Artist2QueryRenderer because it's not a JavaScript reference. We're referencingArtist2_artist
inside a string — delimited by the back-ticks (`) surrounding our GraphQL query. While this name/token is parsed by Relay to assemble queries for the server, it isn't parsed by JavaScript because it's nothing more than text.
Time to celebrate, because Artist2QueryRenderer
no longer has any knowledge of fields it doesn't use! Yay, component isolation 🙌
Convert the section components (Artist2Heading
, Artist2Bio
, and Artist2AuctionResults
) to Fragment Containers
While we removed the data contract from the Artist2QueryRenderer
, Artist2
is still defining a data contract that is too granular for what it's displaying. It's not emitting any fields from the Artist
GraphQL type; why should it be specifying everything its children need?
To resolve this, we're going to repeat the process of converting to Fragment Containers for each of our individual section components. We'll tackle one of these together — the Artist2Heading
— and you'll convert the final two on your own. Each conversion will look very similar to the Artist2
conversion we just completed.
💻 Add an import statement to Artist2Heading
for the react-relay
dependencies we'll need:
import { createFragmentContainer, graphql } from "react-relay"
./Artist2Heading.tsx
💻 Define and export a new Fragment Container:
export const Artist2HeadingFragmentContainer = createFragmentContainer(
Artist2Heading,
{
artist: graphql`
fragment Artist2Heading_artist on Artist {
name
birthYear
}
`,
}
)
./Artist2Heading.tsx
💻 Replace the Artist2HeadingProps
interface with one generated by Relay:
import { Artist2Heading_artist } from "./__generated__/Artist2Heading_artist.graphql"
// ...
interface Artist2HeadingProps {
artist: Artist2Heading_artist
}
./Artist2Heading.tsx
💻 Replace the Artist2Heading
component with the new Artist2HeadingFragmentContainer
in Artist2
:
import { Artist2HeadingFragmentContainer } from './Artist2Heading';
// ...
export const Artist2: React.FC<Artist2Props> = ({ artist }) => {
return (
<div>
<Artist2HeadingFragmentContainer artist={artist} />
<hr />
// ...
./Artist2.tsx
💻 Replace the individual fields specified in the Artist2_artist
query fragment with the Artist2Heading_artist
query fragment:
export const Artist2FragmentContainer = createFragmentContainer(Artist2, {
artist: graphql`
fragment Artist2_artist on Artist {
...Artist2Heading_artist
bio
auctionRecord
auctionLotsSoldAnnually
}
`,
})
Note that we've only replaced the fields that are included in the Artist2Heading_artist
fragment: name
and birthYear
. The remaining fields are still needed because we haven't yet converted the other section components to Fragment Containers.
Yay! There are two more components to convert to Fragment Containers: Artist2Bio
and Artist2AuctionResults
. Take a shot at those on your own! If you get stuck, there are a few places to look for help:
- The two components we already converted
- The
completed
folder for this exercise - Commits in GitHub that were made while building this exercise: for
Artist2
,Artist2Heading
,Artist2Bio
, andArtist2AuctionResults
.
In this exercise we converted four components to Fragment Containers, which allowed us to improve the isolation of our individual components. Relay rolls each of these Fragment Containers up into one query, thanks to our spreading of fragments into their parent components. All these fragments propagate up to the QueryRenderer at the top of our component tree, resulting in one server request that contains all data needed by the entire tree. ✨
Take final note of the components in this exercise, and the GraphQL queries associated with them. Neither Artist2QueryRenderer
nor Artist2
need to specify any actual fields, because they don't use them. The three section components specify only the fields they need to render themselves. 💪🏼 Strong work, friend!
In day-to-day development with Relay, you'll work a lot with Fragment Containers. Whether building a new page/screen or modifying an existing one, you'll use Fragment Containers to specify the contract for data your components are rendering.
As mentioned in the exercise, it's really easy (especially if copy-pasting) to name a fragment incorrectly. Remember: it's the file name, then underscore, then the GraphQL type the fragment is on.
It's incredibly easy to render MyComponent
instead of MyComponentFragmentContainer
in a Relay component tree. This usually shows itself as your display component rendering empty values.