TL;DR; You can reduce the number of queries you make on listing pages by loading content as it gets displayed. Use react-intersection-observer to detect when an element becomes visible and react-content-loader to display a contentful placeholder loader.
You have a developed your backend API and built your React SPA but you find that some pages actually make a lot of requests (either large or lots) to your backend and have become kind of sluggish. Well good news everyone, you can improve that.
Let's consider a typical layout for a "listing" page. What matters here is not the design but the architecture of the page. You have a list of items and then for each item a list of children. All of these must be dynamically loaded from your API.
What are the traditional approaches?
- One big query: You make one fat API query to your backend to load the items and their children (e.g. comments). It works, is perfectly valid for a small number of items but is merely scalable if you expect a high number of children. The biggest problem is being in control of the pagination for the children, which is merely possible when you include children in the parent query. Even with GraphQL, which allows you to paginate children, you would redo the parent query for each new page - this is not optimal.
- Lots of small queries: You load items - or item IDs - using a listing query then for each item you load the item body (if not loaded as part of the first query) and the children. This approach is scalable and gives you better control over pagination.
Option 2 is more fluid in terms of architecture. The problem is you'll end up doing quite a lot of queries on page load depending on the number of items you wish to display on the initial page load. Loading 100 items means you'll end up doing N x 100 queries on your API, where N is the number of correlated resources you need to load for each item.
Considering most of the items will be below the fold, it seems like a huge waste of resources to load everything on page load. A better approach would be to load items as they become visible to users.
Let's do just that.
The useInView hook to the rescue
The react-intersection-observer library provides a hook detecting when an element becomes visible to users.
It has plenty of configuration options - such as configuring a percentage threshold of object height for when to trigger the inView event - but we'll go with the most basic implementation here.
Add react-intersection-observer to your project.
yarn add react-intersection-observer
Now you can use the inView hook on your page items to conditionally load related children.
import React, { FunctionComponent } from 'react';
import { useInView } from 'react-intersection-observer';
interface Props {
item: MyItem;
}
const CommentFeed: FunctionComponent<Props> = ({ item }: Props) => {
// Inject inView in your component. We specify triggerOnce to avoid flicking.
const { ref, inView } = useInView({ triggerOnce: true });
// Load your comments conditionally (hook similar to apollo-graphql in this case)
// It is important to ensure your API query hook supports some sort of skip
// option.
const { loading, data } = useItemComments({ skip: !inView, parentId: item.id });
// Render your component
// We must attach the inView ref on a wrapping div so that useInView can detect
// when this component actually becomes visible to the user
return (
<div ref={ref}>
{data.map(e => <div>{e.body}</div>)}
</div>
);
};
export default CommentFeed;
That's it. You've just saved your backend hundreds of queries potentially.
The useInView hook will guarantee that your API queries are only executed if your element actually becomes visible to the user.
Now we've considerably increased the number of components that will be in a loading state as the user scrolls your page. Therefore let's make that loading state nice and contentful.
Contentful placeholder loader
The react-content-loader loader library allows you to define pulsing SVG objects to be used as a placeholder while your content is loading.
I find this approach nicer than the traditional spinners as it gives your users an indication of what to expect in terms of layout once your content is loaded.
Here is an example of a comment placeholder loader:
The nicest thing about this library is that the author actually developed a site to help you design these SVG loaders. Just go to https://skeletonreact.com and get fancy!
Now let's incorporate that placeholder into our component.
First install the library in your project:
yarn add react-content-loader
Then design your component on https://skeletonreact.com and add it to your project:
import React, { FunctionComponent } from 'react';
import ContentLoader from 'react-content-loader';
const CommentFeedLoader: FunctionComponent = (props) => (
<ContentLoader
speed={2}
width={600}
height={150}
viewBox="0 0 600 150"
backgroundColor="#f5f5f5"
foregroundColor="#ededed"
{...props}
>
<rect x="115" y="10" rx="3" ry="3" width="305" height="13" />
<rect x="9" y="31" rx="3" ry="3" width="66" height="8" />
<rect x="115" y="34" rx="3" ry="3" width="230" height="5" />
<rect x="115" y="46" rx="3" ry="3" width="160" height="5" />
<rect x="115" y="58" rx="3" ry="3" width="122" height="5" />
<rect x="89" y="0" rx="0" ry="0" width="1" height="73" />
</ContentLoader>
)
export default CommentFeedLoader;
Finally conditionally display your loader in your comment feed component:
import React, { FunctionComponent } from 'react';
import { useInView } from 'react-intersection-observer';
import { CommentFeedLoader } from './CommentFeedLoader';
interface Props {
item: MyItem;
}
const CommentFeed: FunctionComponent<Props> = ({ item }: Props) => {
// Inject inView in your component. We specify triggerOnce to avoid flicking.
const { ref, inView } = useInView({ triggerOnce: true });
// Load your comments conditionally (hook similar to apollo-graphql in this case)
// It is important to ensure your API query hook supports some sort of skip
// option.
const { loading, data } = useItemComments({ skip: !inView, parentId: item.id });
// Return placeholder if content is loading or has not been viewed yet
if (loading || !inView) {
return <CommentFeedLoader />
}
// Render your component
// We must attach the inView ref on a wrapping div so that useInView can detect
// when this component actually becomes visible to the user
return (
<div ref={ref}>
{data.map(e => <div>{e.body}</div>)}
</div>
);
};
export default CommentFeed;
That's it!
Your comment feeds now load dynamically as users scroll your page and a nice placeholder loader lets them know it's coming!
Top comments (0)