LoadMore with GraphQL and Apollo
One of the things, I like about Apollo is how simple it is. Unfortunately, simplicity as everything in life has a cost. Regarding Apollo, the price is a bit more boilerplate.
One good example of this is how loadMore
is handled by Apollo. Here is a simple example of loading more data with Apollo.
import PostItem from 'components/PostItem';
import QUERY from './Query.graphql';
import { compose, graphql } from 'react-apollo';
export function Content({ data, loadMore }) {
if (data.loading) {
return <div>Loading...</div>;
}
return (
<v>
<h1>Posts</h1>
<div>
{data.allPosts.edges.map(({ node }) => <PostItem key={node.id} post={node} />)}
</div>
<button onClick={loadMore}>Load more</button>
</div>
);
}
export default compose(
graphql(QUERY, {
props: ({ data }) => ({
loadMore: () => {
return data.fetchMore({
variables: {
cursor: data.allPosts.pageInfo.endCursor,
},
updateQuery(previousResult, { fetchMoreResult }) {
const connection = fetchMoreResult.allPosts;
return {
allPosts: {
edges: [...previousResult.allPosts.edges, ...connection.edges],
pageInfo: connection.pageInfo,
},
};
},
});
},
}),
}),
)(Content);
(Note: I'm using the Relay.Connection interface)
The code is quite simple, but there is LOT of it. Especially, when you have similar code snippets in 20 or 30 other places.
So, how can we simplify this? ?
Step 1 - withLoading
Lets start with an obvious extraction withLoading
HOC component:
// utils/graphql.js
export function withLoading(Component) {
return (props) => {
if (props.data.loading) {
return <div>Loading...</div>;
}
return <Component {...props} />;
}
}
(Note: I collect all my GraphQL helper functions in utils/graphql.js
)
withLoading
is something which will be needed everywhere Apollo is fetching data.
Our component is starting to get simpler.
export function Content({ data, loadMore }) {
return (
<div>
<h1>Posts</h1>
<div>
{data.allPosts.edges.map(({ node }) => <PostItem key={node.id} post={node} />)}
</div>
<button onClick={loadMore}>Load more</button>
</div>
);
}
export default compose(
graphql(QUERY, {
// ... code ...
}),
withLoading,
)(Content);
Step 2 - mapNodes
{data.allPosts.edges.map(({ node }) => <PostItem key={node.id} post={node} />)}
Having to map over edges.node
is something we will have to do for every connection. So it makes sense to extract it away.
In this way, we completely hide the knowledge about Relay connection structure. ?
// utils/graphql.js
export function mapNodes(connection, fn) {
return connection.edges.map(({ node }) => fn(node));
}
Step 3 - loadMore
Now, let's go to the fun part. Generalizing loadMore
. ?
I have marked with [brackets] all parts of the functions that are unique to this component.
graphql(QUERY, {
props: ({ data }) => ({
loadMore: () => {
return [data.fetchMore]({
variables: {
cursor: [data].[allPosts].pageInfo.endCursor,
},
updateQuery(previousResult, { fetchMoreResult }) {
const connection = fetchMoreResult.[allPosts];
return {
[allPosts]: {
edges: [...previousResult.[allPosts].edges, ...connection.edges],
pageInfo: connection.pageInfo,
},
};
},
});
},
}),
}),
As you can see - data
and allPosts
are the only two unique variables. So we can create a function which accepts them as arguments:
// utils/graphql.js
export function loadMore(data, path) {
return data.fetchMore({
variables: {
cursor: data[path].pageInfo.endCursor,
},
updateQuery(previousResult, { fetchMoreResult }) {
const connection = fetchMoreResult[path];
return {
[path]: {
edges: [...previousResult[path].edges, ...connection.edges],
pageInfo: connection.pageInfo,
},
};
},
});
}
(Note: Use lodash.setWith to add support for nested paths like: 'viewer.allPosts')
With this change, our code looks a lot more simple:
export function Content({ data, loadMore }) {
return (
<div>
<h1>Posts</h1>
<div>
{mapNodes(data.allPosts, post => <PostItem key={post.id} post={post} />)}
</div>
<button onClick={loadMore}>Load more</button>
</div>
);
}
export default compose(
graphql(QUERY, {
props: ({ data }) => ({
loadMore: () => loadMore(data, 'allPosts')
}),
}),
withLoading,
)(Content);
Step 4 - LoadMoreButton
But we can go even further.
Having a button which triggers loadMore
seems like a pretty standard thing in every application.
What if we just extract a LoadMoreButton
component.
class LoadMoreButton extends React.Component {
render() {
const { data, path, children, ...otherProps } = this.props;
return (
<button {...otherProps} onClick={this.onClick}>
{children || 'Load more'}
</button>
);
}
onClick = () => {
loadMore(this.props.data, this.props.path);
}
}
Then our code becomes just this.
export function Content({ data }) {
return (
<div>
<h1>Posts</h1>
<div>
{mapNodes(data.allPosts, post => <PostItem key={post.id} post={post} />)}
</div>
<LoadMoreButton data={data} path="allPosts" />
</div>
);
}
export default compose(
graphql(QUERY),
withLoading,
)(Content);
This is so much better than our initial version ?
Conclusion
Here is a gist of the final code.
When I'm working with Apollo, I spend a lot of time to look for patterns and functionality that can be generalized and extracted. This approach helped me a lot when I was integrating GraphQL in Product Hunt.
Ping me on Twitter if you have any questions or comments ?