skip to Main Content

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.

  1. 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;
}







  1. 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
        }
      }
   
    }
  }
`;


  1. 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;



  1. 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;

  1. Here is the error I get when I try to deploy my code to Vercel

enter image description here

  1. 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);


  1. 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


  1. 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 you

    Login or Signup to reply.
  2. as I said before. You are not exporting the endCursor function on you api.ts file.

    You are trying to use it on your Queries.ts, here:

    import { getBlogPosts, endCursor } from "./api";
    

    but if you look inside your api.ts file, you wont see any function called endCursor.

    So, the error text is pretty clear.

    Maybe you removed that function by mistake or something…

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search