Loadable fields
Overview
Loadable fields are a fundamental Isograph concept.
Client fields can be selected loadably (e.g. BlogBody @loadable
). This means that the data for that BlogBody
client field will not be included in the parent network request. Instead, the outer client field (BlogPostDisplay
) will receive a LoadableField
in place of that loadably selected client field.
This LoadableField
is a wrapper around a function that, when called, will make a network request for the data needed by the BlogPost
client field.
You usually would not call the LoadableField
yourself. Instead, pass it to a hook that knows what to do with it!
Basic walk-through
In this example, we'll design a BlogPostDisplay
component client field that renders a BlogPostHeader
immediately, and defers the JavaScript and data for a BlogPostBody
.
Let's start by defining a BlogPostDisplay
component without loadable fields:
import { iso } from '@iso';
export const BlogPostDisplay = iso(`
field BlogPost.BlogPostDisplay {
BlogHeader
BlogBody
}
`)((blogPost) => {
return (
<>
<blogPost.BlogHeader />
<blogPost.BlogBody />
</>
);
});
Now, we can add @loadable
to BlogBody
as follows:
import { iso } from '@iso';
export const BlogPostDisplay = iso(`
field BlogPost.BlogPostDisplay {
BlogHeader
BlogBody @loadable
}
`)((blogPost) => {
return (
<>
<blogPost.BlogHeader />
{/* uh oh, fails to type check */}
<blogPost.BlogBody />
</>
);
});
Uh oh! <blogPost.BlogBody />
has started to fail to type check. That's because we've received a loadable field instead of the component directly.
The first thing we should do is to pass blogPost.BlogBody
to useClientSideDefer
, which returns a fragment reference.
We can pass this fragment reference to useResult
to read its result. But, that would cause the component to suspend. Since we want to render the BlogHeader
immediately, we shouldn't do that.
So, instead let's pass it to FragmentReader
, and wrap that in a Suspense
boundary:
import { iso } from '@iso';
import { FragmentReader, useClientSideDefer } from '@isograph/react';
export const BlogPostDisplay = iso(`
field BlogPost.BlogPostDisplay {
BlogHeader
BlogBody @loadable
}
`)((blogPost) => {
const fragmentReference = useClientSideDefer(
blogPost.BlogBody,
// any parameters that were not passed to BlogBody above
// can we passed here. There are none, so we just pass an empty
// object.
{},
);
return (
<>
<blogPost.BlogHeader />
<React.Suspense fallback={'Loading...'}>
<FragmentReader fragmentReference={fragmentReference} />
</React.Suspense>
</>
);
});
Great! Now when this component is initially rendered, we'll make a network request for the data required by the BlogPostBody
. While that request is in flight, we render a suspense fallback. When that network request completes, the BlogPostBody
is rendered.
The final step is to change @loadable
to @loadable(lazyLoadArtifact: true)
.
import { iso } from '@iso';
import { FragmentReader, useClientSideDefer } from '@isograph/react';
export const BlogPostDisplay = iso(`
field BlogPost.BlogPostDisplay {
BlogHeader
BlogBody @loadable(lazyLoadArtifact: true)
}
`)((blogPost) => {
const fragmentReference = useClientSideDefer(
blogPost.BlogBody,
// any parameters that were not passed to BlogBody above
// can we passed here. There are none, so we just pass an empty
// object.
{},
);
return (
<>
<blogPost.BlogHeader />
<React.Suspense fallback={'Loading...'}>
<FragmentReader fragmentReference={fragmentReference} />
</React.Suspense>
</>
);
});
Awesome! Now, in addition to making a request for the BlogPostBody
data, we will also make a request for the component JavaScript (if it has not been fetched before.)
Imperatively fetching
In addition to fetching during render, you can also pass the loadable field to useImperativeLoadableField
to fetch it in response to an event (such as a click):
import { iso } from '@iso';
import { FragmentReader, useImperativeLoadableField } from '@isograph/react';
import { UNASSIGNED_STATE } from '@isograph/react-disposable-state';
export const BlogPostDisplay = iso(`
field BlogPost.BlogPostDisplay {
BlogHeader
BlogBody @loadable(lazyLoadArtifact: true)
}
`)((blogPost) => {
const { fragmentReference, loadField } = useImperativeLoadableField(
blogPost.BlogBody,
);
return (
<>
<blogPost.BlogHeader />
{fragmentReference != UNASSIGNED_STATE ? (
<button
onClick={() =>
loadField(
// any parameters that were not passed to BlogBody above
// can we passed here. There are none, so we just pass an empty
// object.
{},
)
}
>
Load blog body
</button>
) : (
<React.Suspense fallback={'Loading...'}>
<FragmentReader fragmentReference={fragmentReference} />
</React.Suspense>
)}
</>
);
});
Pagination
Pagination is also built on loadable fields. See the pagination docs.
Data-driven dependencies
Check out the data driven dependencies documentation to see how to combine @loadable
fields, pagination and asConcreteType
fields to fetch the minimal amount of data and JavaScript needed!