I am trying to create a pagination function on my Next JS blog, I have been able to limit the Query output but get errors when I try implementing the load more button. My stack is GraphQL, WordPress and Next JS
Here is my code below.
- api.ts
Here is where I make my API call, this is not all but I am trying to extract the most important part for the sake of clarity.
api.ts
import { homepageQuery, blogPosts, blogPostBySlug } from './queries';
const API_URL = process.env.WP_API_URL;
async function fetchAPI(query, { variables = {} } = {}) {
// Set up some headers to tell the fetch call
// that this is an application/json type
const headers = { 'Content-Type': 'application/json' };
// build out the fetch() call using the API_URL
// environment variable pulled in at the start
// Note the merging of the query and variables
const res = await fetch(API_URL, {
method: 'POST',
headers,
body: JSON.stringify({ query, variables }),
});
// error handling work
const json = await res.json();
if (json.errors) {
console.log(json.errors);
console.log('error details', query, variables);
throw new Error('Failed to fetch API');
}
return json.data;
}
export async function getHomepageSections() {
const data = await fetchAPI(homepageQuery);
return data;
}
export async function getBlogPosts(endCursor = null, taxonomy = null) {
const data = await fetchAPI(blogPosts, { variables: {after:endCursor} });
return data;
}
// export async function getBlogPosts(endCursor = null, first = 12, taxonomy = null) {
// const condition = `after: "${endCursor}", first: ${first}, where: {orderby: {field: DATE, order: DESC}}`;
// const data = await fetchAPI(blogPosts, { variables: { condition } });
// return data;
// }
export async function getBlogPostBySlug(slug) {
const data = await fetchAPI(blogPostBySlug, { variables: { id: slug } });
return data;
}
- Queries.ts is Where I houses my GraphQL Query
import { getBlogPosts, endCursor } from "./api";
export const homepageQuery = `
query HomepageQuery {
homepageSections {
edges {
node {
homepage {
hero {
animatedwords
heading
subtitle
}
callouts {
title
subtitle
calloutone {
title
subtext
image {
mediaItemUrl
}
}
calloutthree {
title
subtext
image {
mediaItemUrl
}
}
callouttwo {
title
subtext
image {
mediaItemUrl
}
}
}
icongrid {
iconone {
description
icon
title
}
iconfive {
description
icon
title
}
iconfour {
description
icon
title
}
iconsix {
description
icon
title
}
iconthree {
description
icon
title
}
icontwo {
description
icon
title
}
}
}
}
}
}
}
`;
let condition = `after: "$endCursor",first: 12, where: {orderby: {field: DATE, order: DESC}}`
export const blogPosts = `
query BlogPosts{
posts(${condition}) {
edges {
node {
author {
node {
nickname
}
}
date
slug
featuredImage {
node {
mediaItemUrl
}
}
title
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
`;
export const blogPostBySlug = `
query PostBySlug($id: ID!) {
post(id: $id, idType: SLUG) {
id
content
title
author {
node {
nickname
avatar {
url
}
}
}
date
featuredImage {
node {
mediaItemUrl
}
}
}
}
`;
- Loadmore.js is where I house my Load More Button
import React from 'react';
import { getBlogPosts } from '../../../../lib/api';
const LoadMore = ({allPosts, setallPosts}) => {
const handleOnclick = async(event) =>{
const morePosts = await getBlogPosts(allPosts.pageInfo.endCursor);
let updatedPosts = {
pageInfo: {
},
nodes: []
}
updatedPosts.pageInfo = morePosts.pageInfo;
allPosts.nodes.map((node) => {
updatedPosts.nodes.push(node);
});
morePosts.nodes.map((node) => {
updatedPosts.nodes.push(node);
});
setallPosts(updatedPosts);
}
return (
<>
<button
onClick={handleOnclick}
>
Load More
</button>
</>
);
};
export default LoadMore;
- Result.ts is where I display my blog post list.
import React from 'react';
import Link from 'next/link';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import {
colors,
useMediaQuery,
FormControl,
OutlinedInput,
InputAdornment,
Button,
Avatar,
Typography,
Grid,
Divider,
} from '@material-ui/core';
import { Icon, Image } from 'components/atoms';
import { CardProduct, Section, SectionAlternate } from 'components/organisms';
import { formatDate } from '../../../../../utils';
import { LoadMore } from 'components/molecules';
import {useState} from 'react';
const useStyles = makeStyles(theme => ({
link: {
cursor: 'pointer',
},
pagePaddingTop: {
padding: theme.spacing(3),
paddingBottom: theme.spacing(3),
[theme.breakpoints.up('md')]: {
paddingTop: theme.spacing(5),
paddingBottom: theme.spacing(5),
},
},
sectionAlternate: {
'& .section-alternate__content': {
padding: theme.spacing(3),
paddingBottom: theme.spacing(3),
[theme.breakpoints.up('md')]: {
paddingTop: theme.spacing(5),
paddingBottom: theme.spacing(5),
},
},
},
searchInputContainer: {
background: theme.palette.alternate.main,
padding: theme.spacing(2),
boxShadow: '0 4px 14px 0 rgba(0, 0, 0, 0.11)',
borderRadius: theme.spacing(1),
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
'& .MuiOutlinedInput-notchedOutline': {
border: '0 !important',
},
'& .MuiInputAdornment-positionStart': {
marginRight: theme.spacing(2),
},
'& .MuiOutlinedInput-adornedStart': {
paddingLeft: 0,
},
'& .MuiOutlinedInput-input': {
padding: 0,
},
[theme.breakpoints.down('sm')]: {
padding: theme.spacing(1),
},
},
searchButton: {
maxHeight: 45,
minWidth: 135,
[theme.breakpoints.down('sm')]: {
minWidth: 'auto',
},
},
cardProduct: {
display: 'flex',
flexDirection: 'column',
height: '100%',
borderRadius: theme.spacing(1),
'& .card-product__content': {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
},
},
image: {
objectFit: 'cover',
borderRadius: theme.spacing(0, 0, 20, 0),
},
blogContent: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
list: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
avatarContainer: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
avatar: {
marginRight: theme.spacing(1),
},
divider: {
margin: theme.spacing(2, 0),
},
button: {
minWidth: '100%',
maxWidth: '100%',
[theme.breakpoints.up('sm')]: {
minWidth: 420,
},
},
answerCount: {
padding: theme.spacing(1 / 2, 1),
borderRadius: theme.spacing(1),
background: theme.palette.secondary.light,
color: 'white',
fontWeight: 300,
},
}));
const Result = ({
data,
className,
posts,
...rest
}: ViewComponentProps): JSX.Element => {
const [allPosts, setallPosts] = useState(posts)
const classes = useStyles();
const theme = useTheme();
const isMd = useMediaQuery(theme.breakpoints.up('md'), {
defaultMatches: true,
});
const BlogMediaContent = (props: ImageProps) => {
return (
<Image
{...props}
src={props?.node?.featuredImage?.node?.mediaItemUrl}
className={classes.image}
lazyProps={{ width: '100%', height: '100%' }}
/>
);
};
const BlogContent = (props: any) => (
<div className={classes.blogContent}>
<Typography variant="h6" color="textPrimary" gutterBottom>
{props.title}
</Typography>
<Typography variant="body1" color="textSecondary">
{props.subtitle}
</Typography>
<div style={{ flexGrow: 1 }} />
<Divider className={classes.divider} />
<div className={classes.list}>
<div className={classes.avatarContainer}>
<Avatar {...props.author.photo} className={classes.avatar} />
<Typography variant="body2" color="textPrimary">
{props.author}
</Typography>
</div>
<Typography variant="overline" color="textSecondary">
{props.date}
</Typography>
</div>
</div>
);
return (
<div className={className} {...rest}>
<SectionAlternate className={classes.sectionAlternate}>
<Grid container spacing={isMd ? 4 : 2}>
{allPosts.map((post: any, index: number) => (
<Grid
className={classes.link}
item
xs={12}
sm={6}
md={4}
key={index}
data-aos="fade-up"
>
<Link href={`/blog/${post.node.slug}`}>
<CardProduct
withShadow
liftUp
className={classes.cardProduct}
mediaContent={
<BlogMediaContent {...post} alt={post.node.title} />
}
cardContent={
<BlogContent
title={post.node.title}
// subtitle={item.subtitle}
author={'copyandpost'}
date={formatDate(post.node.date)}
/>
}
/>
</Link>
</Grid>
))}
{/* <Grid item xs={12} container justify="center">
<Button
variant="contained"
color="primary"
size="large"
className={classes.button}
>
Load more
</Button>
</Grid> */}
<LoadMore allPosts={allPosts} setallPosts={setallPosts}/>
</Grid>
</SectionAlternate>
</div>
);
};
export default Result;
- Here is the error I get when I try to deploy my code to Vercel
- Another error when I try to click the loadmore button.
srccomponentsmoleculesLoadMoreLoadMore.tsx (13:59) @ endCursor
11 |
12 |
> 13 | const morePosts = await getBlogPosts(allPosts.pageInfo.endCursor);
- Here is the error I find in my console.
LoadMore.tsx:13 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'endCursor')
at handleOnclick (LoadMore.tsx:13:60)
at HTMLUnknownElement.callCallback (react-dom.development.js:4164:14)
at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:16)
at invokeGuardedCallback (react-dom.development.js:4277:31)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:4291:25)
at executeDispatch (react-dom.development.js:9041:3)
at processDispatchQueueItemsInOrder (react-dom.development.js:9073:7)
at processDispatchQueue (react-dom.development.js:9086:5)
at dispatchEventsForPlugins (react-dom.development.js:9097:3)
at eval (react-dom.development.js:9288:12)
at batchedUpdates$1 (react-dom.development.js:26135:12)
at batchedUpdates (react-dom.development.js:3991:12)
at dispatchEventForPluginEventSystem (react-dom.development.js:9287:3)
at dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay (react-dom.development.js:6465:5)
at dispatchEvent (react-dom.development.js:6457:5)
at dispatchDiscreteEvent (react-dom.development.js:6430:5)
Any help I get would be highly appreciated
2
Answers
The error is clear… you are not exporting the endCursor function on you
api.ts
file.if you could post the complete
api.ts
file it will be better for helping youas I said before. You are not exporting the
endCursor
function on youapi.ts
file.You are trying to use it on your
Queries.ts
, here:but if you look inside your
api.ts
file, you wont see any function calledendCursor
.So, the error text is pretty clear.
Maybe you removed that function by mistake or something…