Skip to content

Instantly share code, notes, and snippets.

@trevorblades
Created October 5, 2020 19:17
Show Gist options
  • Save trevorblades/9f76f133147fa6d712e28d61ec5666be to your computer and use it in GitHub Desktop.
Save trevorblades/9f76f133147fa6d712e28d61ec5666be to your computer and use it in GitHub Desktop.
Pagination helpers in AC3
import fetch from 'isomorphic-fetch';
import ws from 'isomorphic-ws';
import {
ApolloClient,
ApolloLink,
HttpLink,
InMemoryCache,
split
} from '@apollo/client';
import {WebSocketLink} from '@apollo/client/link/ws';
import {
getMainDefinition,
offsetLimitPagination
} from '@apollo/client/utilities';
const httpLink = new HttpLink({
uri: process.env.GATSBY_API_URL,
fetch
});
const authLink = new ApolloLink((operation, forward) => {
const token = localStorage.getItem(process.env.GATSBY_TOKEN_KEY);
if (token) {
operation.setContext({
headers: {
Authorization: `Bearer ${token}`
}
});
}
return forward(operation);
});
const wsLink = new WebSocketLink({
uri: process.env.GATSBY_WS_URL,
webSocketImpl: ws,
options: {
reconnect: true,
connectionParams: () => ({
authToken: localStorage.getItem(process.env.GATSBY_TOKEN_KEY)
})
}
});
const splitLink = split(
({query}) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
authLink.concat(httpLink)
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
packs: offsetLimitPagination(['sort', 'type'])
}
}
}
}),
resolvers: {
Query: {
isLoggedIn: () =>
Boolean(localStorage.getItem(process.env.GATSBY_TOKEN_KEY))
},
Pack: {
fullName: pack =>
(pack.collection ? `${pack.collection.name}: ` : '') + pack.name
}
}
});
export default client;
import PackGrid, {CardSkeleton, PacksLoading} from '../PackGrid';
import PackLoader from './PackLoader';
import PropTypes from 'prop-types';
import React, {useEffect, useState} from 'react';
import {NetworkStatus, gql, useQuery} from '@apollo/client';
import {PACK_LISTING_FRAGMENT} from '../../utils';
import {Text} from '@chakra-ui/core';
const LIST_PACKS = gql`
query ListPacks(
$limit: Int!
$offset: Int!
$sort: SortOption
$type: PackType
) {
packs(limit: $limit, offset: $offset, sort: $sort, type: $type) {
...PackListingFragment
}
}
${PACK_LISTING_FRAGMENT}
`;
export default function ListPacks({sort, type}) {
const [loaded, setLoaded] = useState(false);
const {data, error, fetchMore, networkStatus, variables} = useQuery(
LIST_PACKS,
{
notifyOnNetworkStatusChange: true,
variables: {
sort,
type,
offset: 0,
limit: 10
}
}
);
useEffect(() => {
setLoaded(false);
}, [sort, type]);
if (
networkStatus === NetworkStatus.loading ||
networkStatus === NetworkStatus.refetch ||
networkStatus === NetworkStatus.setVariables
) {
return <PacksLoading />;
}
if (error) {
return <Text color="red.500">{error.message}</Text>;
}
return (
<PackGrid packs={data.packs}>
{!loaded && data.packs.length % variables.limit === 0 && (
<PackLoader
setLoaded={setLoaded}
loading={networkStatus === NetworkStatus.fetchMore}
fetchMore={fetchMore}
offset={data.packs.length}
>
<CardSkeleton />
</PackLoader>
)}
</PackGrid>
);
}
ListPacks.propTypes = {
sort: PropTypes.string.isRequired,
type: PropTypes.string.isRequired
};
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef} from 'react';
export default function PackLoader({
children,
offset,
loading,
fetchMore,
setLoaded
}) {
const loaderRef = useRef();
const loadMore = useCallback(
entries => {
const target = entries[0];
if (target.isIntersecting && !loading) {
fetchMore({
variables: {
offset
}
}).then(({data}) => setLoaded(!data.packs.length));
}
},
[loading, fetchMore, offset, setLoaded]
);
useEffect(() => {
// this technique is inspired by
// https://medium.com/@swatisucharita94/react-infinite-scroll-with-intersection-observer-api-db3998e52d63
const observer = new IntersectionObserver(loadMore, {
root: null,
rootMargin: '0px',
threshold: 0.25
});
const {current} = loaderRef;
observer.observe(current);
return () => observer.unobserve(current);
}, [loaderRef, loadMore]);
return (
<>
{React.cloneElement(children, {ref: loaderRef})}
{Array.from({length: 2}, (item, index) =>
React.cloneElement(children, {key: index})
)}
</>
);
}
PackLoader.propTypes = {
children: PropTypes.element.isRequired,
offset: PropTypes.number.isRequired,
fetchMore: PropTypes.func.isRequired,
setLoaded: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment