React - pagination

I have an array of pages that have at most ten images.

I need to add data or a class to the zeroth index in the array and set it as the first page. I also need buttons to change the state and the “page”. To set the state like maybe:

setState({
  pageNumber: pageIndex[arr]
})

In app.js, this is what I have to split the data into pages:

  getGifList = async () => {
    const gifListJson = await fetch(`${API}/my-route`)
    const gifList = await gifListJson.json()
    const pageLimit = 10
    const gifPages = []

    const splitPages = () => {
      while (gifList.length) {
      gifPages.push(gifList.splice(0, pageLimit))
      }
      return gifPages
    }

    this.setState({
      gifList: splitPages()
    })
  }

and here is the list:

export default class gifList extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      ...this.state,
      pageNumber: this.props.pageNumber
    }
  }

  pageCheck = () => {
    let page = this.state.pageNumber

    this.props.gifList.forEach(
      (gifPage, idx) => {
        console.log(gifPage, idx)

        
        
        // gifPage.map(
        //   (gif, idx) => {
        //     return <div><Gif gif={gif} key={idx} /></div>
        //   })
      }
    )
  }
  

  render () {
    return (
      <div className="gifList">
        <div className="wrapper">
          {this.pageCheck()}
        </div>
        <footer className="pageButtonsContainer">
          <button>1</button>
          <button>2</button>
          <button>3</button>
        </footer>
      </div>
    )
  }
}

Am I thinking about this wrong? I’m just tired :slight_smile:

You already got the array of pages.

Now you loop through them to create the paging links.

I think you can just create a JS paging function and then call that function from React code. The work of this function will be to create your paging links, show which page the user is currently, the previous and next pages and so on.

This is something like done in this pagination tutorial.

I’m currently dealing with this on a “media feed” portfolio project I’m working on :laughing:

Your solution is basically what I’m employing at the moment as well, it seems to work fine. The only caveat to this is that I’m testing my app with relatively few return objects. I’m kinda thinking loading up an array of return results into state might get iffy if results start numbering in the hundreds or thousands.

The type of return object I’m dealing with is coming from a mongoDB instance, I’m not sure what your dealing with so I can’t say this will work. But my thought on the matter was to rearrange the Frontend/API a little. One function, fetchResults() or something to that end, listens for page-load/refresh type events and would request a list of document IDs representing the return results, instead of directly returning the actual result objects. This ID list would then get stored in Component state. Another function, say handlePagingButtons() which is listening for ‘Next Page’/‘Last Page’ paging button clicks, would be responsible for crawling through the ID list and requesting the API for the actual page return items from the DB to then populate the app. That way on every new page you’re only requesting a small number of results each time (like 10) instead of hitting the client with potentially tons of results that might not actually be seen. I guess I’m just trying to think “pagination on demand”, but I’m new so what do I know. Again, I’m sorry if this isn’t an option for you.

...
this.state = {
    resultIDs: [],
    pageResults = [],
    pageLimit: 10,
    pageNumber: 0
}
...
componentDidMount() {
    Promise.resolve()
           .then(() => this.fetchResults())
           .then(() => this.handlePagingButtons());
}

async fetchResults() {
    let currentIDs = await fetch(`${API}/results`),
        resultIDs = [];
    currentIDs = await currentIDs.json();
    while (currentIDs.length) {
        resultIDs.push(currentIDs.splice(0, this.state.pageLimit))
    }
    this.setState({resultIDs});

}

async handlePagingButtons(e) {
    let pageNumber = this.state.pageNumber;
    if (e) {
        if (e.target.name == "next-page") pageNumber++
        else {
            if (pageNumber == 0) return console.log('Already On First Page');
            pageNumber--;
        }
    }
    if (this.state.currentIDs[pageNumber]) {
        let pageResults = await fetch(`${API}/results?ids=${this.state.currentIDs[pageNumber]}`);
        pageResults = await pageResults.json();
        this.setState({pageNumber, pageResults})
    }
    else console.log("No More Results To Display")
}

I am trying to use your method, but something is not working properly…anything you see?

import React from 'react'
import Gif from './gif'
import './gifList.css'

const API = "http://localhost:3000"

export default class gifList extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      ...this.state,
      gifList: [],
      currentDisplay: [],
      pageLimit: 10,
      pageNumber: 0
    }
  }

  //// Grab the Gif API \\\\
  getGifList = async () => {
    const gifListJson = await fetch(`${API}/my-route`)
    const gifList = await gifListJson.json()
    let display = this.state.currentDisplay

    while (gifList.length) {
      display.push(gifList.splice(0, this.state.pageLimit))
    }
    this.setState({
      currentDisplay: display[this.state.pageNumber]
    })
  }

  componentDidMount = async () => {
    await this.getGifList()
    await this.handlePages()
  }

  //// Next/prev page of X # of gifs
  handlePages = async (ev) => {
    let pageNumber = this.state.pageNumber
    if (ev) {
      pageNumber++
    } else {
      if (pageNumber === 0) {
        pageNumber--
      }
    }
    if (this.state.gifList[pageNumber]) {
      const pageResults = await fetch(`${API}/my-route?ids=${this.state.gifList[pageNumber]}`)
      const pages = await pageResults.json()
      console.log(pages)

      this.setState({
        pageNumber,
        pages
      })
    }
  }

  render () {
    return (
      <div className="gifList">
        <div className="wrapper">
          {
            this.state.currentDisplay.map((gif, idx) => {
              return <div key={idx}><Gif gif={gif} id={idx} /></div>
            })
          }
        </div>
        <footer className="pageButtonsContainer">
          <button onClick={this.handlePages}>Prev</button>
          <button onClick={this.handlePages}>Next</button>
        </footer>
      </div>
    )
  }
}

It kinda seems like your mixing variables a little. In my proposal fetchResults() (your getGifList() I think), retrieves a full list of IDs representing all the GIFs available, formats the list into groups of 10 (pages), and saves the whole list in state. The app (display) isn’t actually populated with any real GIFs or json objects yet.
Something like this.state.gifList = [['ID1', 'ID2', 'ID3', 'ID4', 'ID5', 'ID6', 'ID7', 'ID8' 'ID9', 'ID10 ], ['ID11', 'ID12', 'ID13']]
In your getGifList(), it appears you are pulling all the actual GIFs, formating the list of GIFs into pages, and setting the currentDisplay to the first page. Then handlePages() is trying to get a list of IDs from this.state.gifList, but getGifList() never actually set this.state.gifList to anything, so it always remains an empty list. You might also run into an issue with componentDidMount as it will run getGifList() and handlePages() simultaneously, but theoretically handlePages() is dependent on the state of this.state.gifList set by getGifList(). So getGifList() needs to run before handlePages().

import React from 'react'
import Gif from './gif'
import './gifList.css'

const API = "http://localhost:3000"

export default class gifList extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      ...this.state,
      gifList: [],
      currentDisplay: [],
      pageLimit: 10,
      pageNumber: 0
    }
  }

  //// Grab the Gif API \\\\
  getGifList = async () => {
    /* Fetches a GIF results ID list formatted into pages of length pageLimit.
       Does not return any actual GIFs */
    let gifListIDs = await fetch(`${API}/my-route`);  // This needs to return a list of GIF IDs to represent results
    gifListIDs = await gifListJson.json();
    let gifList = [];
    while (gifListIDs.length) {
      gifList.push(gifListIDs.splice(0, this.state.pageLimit))
    }
    this.setState({gifList})
  }

  componentDidMount() => {
    /* On page load fetch GIF IDs then populate app with first page of GIFs. */
      Promise.resolve()
        .then(this.getGifList())
        .then(this.handlePages())
  }

  //// Next/prev page of X # of gifs
  handlePages = async (ev) => {
    /* Populates the GIF display by fetching GIF results for IDs within a specific page */
    let pageNumber = this.state.pageNumber
    if (ev) {
      pageNumber++
    } else {
      if (pageNumber === 0) {
        pageNumber--
      }
    }
    if (this.state.gifList[pageNumber]) {
      let currentDisplay = await fetch(`${API}/my-route?ids=${this.state.gifList[pageNumber]}`);
      currentDisplay = await pageResults.json();
      console.log(pages);

      this.setState({
        pageNumber,
        currentDisplay
      })
    }
  }

  render () {
    return (
      <div className="gifList">
        <div className="wrapper">
          {
            this.state.currentDisplay.map((gif, idx) => {
              return <div key={idx}><Gif gif={gif} id={idx} /></div>
            })
          }
        </div>
        <footer className="pageButtonsContainer">
          <button onClick={this.handlePages}>Prev</button>
          <button onClick={this.handlePages}>Next</button>
        </footer>
      </div>
    )
  }
}

Yeah, I’ve realized that :frowning:
I know I am so close with this current way. I just want to call all of them at once, which is gifList(22 images). If I hardcode pageNumber to be 1 or 2, they go to the 2nd and 3rd page respectively. First page shows 10 gifs, second shows 10, third shows 2.
30%20PM

When I click on the buttons, it changes the state of pageNumber, but the currentDisplay list does not change (even though the pageNumber state changes on click)

I believe it is a lifecycle problem of the component not rerendering after the state is changed?
I believe I’m closer now and not doing the second call you have in your solution…

import React from 'react'
import Gif from './gif'
import './gifList.css'

const API = "http://localhost:3000"

export default class gifList extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      ...this.state,
      gifList: [],
      currentDisplay: [],
      pageLimit: 10,
      pageNumber: 0
    }
  }

  //// Grab the Gif API \\\\
  getGifList = async () => {
    const gifListJson = await fetch(`${API}/my-route`)
    const gifListSplitToPages = await gifListJson.json()
    const gifList = gifListSplitToPages.slice()
    const display = this.state.currentDisplay

    this.setState({
      gifList: gifList
    })

    // If there are gifs in the list, split them by 10 into the currentDisplay
    while (gifListSplitToPages.length) {
      display.push(gifListSplitToPages.splice(0, this.state.pageLimit))
    }
    this.setState({
      currentDisplay: display[this.state.pageNumber]
    })
  }

  componentDidMount = async () => {
    await this.getGifList()
    await this.handlePages()
  }

  //// Next/prev page of 10 # of gifs
  handlePages = async (e) => {
    const display = this.state.currentDisplay
    const pageNumber = this.state.pageNumber

    if (e) {
      if (e.target.className === "next") {
        this.setState({
          pageNumber: this.state.pageNumber + 1,
          currentDisplay: display
        })
      } 
      if (e.target.className === "prev" && pageNumber > 0) {
        this.setState({
          pageNumber: this.state.pageNumber - 1,
          currentDisplay: display
        })
      }
      if (e.target.className === "prev" && pageNumber === 0) {
        alert(`You are on the first page!`)
      }
    }
  }

  render () {
    return (
      <div className="gifList">
        <div className="wrapper">
          {
            this.state.currentDisplay.map((gif, idx) => {
              return <div key={idx}><Gif gif={gif} id={idx} /></div>
            })
          }
        </div>
        <footer className="pageButtonsContainer">
          <button onClick={this.handlePages} className="prev">Prev</button>
          <button onClick={this.handlePages} className="next">Next</button>
        </footer>
      </div>
    )
  }
}

I updated currentDisplay: display[this.state.pageNumber] in handlePages() and now I get the error after I hit the “Next” button that in the rendering of the jsx, I get TypeError: this.state.currentDisplay.map is not a function.

I would expect this to happen because now the component is trying to update but cannot because it doesn’t know what currentDisplay is yet. What can I do to solve this??

import React from 'react'
import Gif from './gif'
import './gifList.css'

const API = "http://localhost:3000"

export default class gifList extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      ...this.state,
      gifList: [],
      currentDisplay: [],
      pageLimit: 10,
      pageNumber: 0
    }
  }

  //// Grab the Gif API \\\\
  getGifList = async () => {
    const gifListJson = await fetch(`${API}/my-route`)
    const gifListSplitToPages = await gifListJson.json()
    const gifList = gifListSplitToPages.slice()
    const display = this.state.currentDisplay

    // If there are gifs in the list, split them by 10 into the currentDisplay
    while (gifListSplitToPages.length) {
      display.push(gifListSplitToPages.splice(0, this.state.pageLimit))
    }

    this.setState({
      currentDisplay: display[this.state.pageNumber],
      gifList: gifList
    })
  }

  componentDidMount = async () => {
    await this.getGifList()
    await this.handlePages()
  }

  //// Next/prev page of 10 # of gifs
  handlePages = async (e) => {
    const display = this.state.currentDisplay
    const pageNumber = this.state.pageNumber

    if (e) {
      if (e.target.className === "next") {
        this.setState({
          pageNumber: this.state.pageNumber + 1,
          currentDisplay: display[this.state.pageNumber]
        })
      } 
      if (e.target.className === "prev" && pageNumber > 0) {
        this.setState({
          pageNumber: this.state.pageNumber - 1,
          currentDisplay: display[this.state.pageNumber]
        })
      }
      if (e.target.className === "prev" && pageNumber === 0) {
        alert(`You are on the first page!`)
      }
    }
  }

  render () {
    return (
      <div className="gifList">
        <div className="wrapper">
          {
            this.state.currentDisplay.map((gif, idx) => {
              return <div key={idx}><Gif gif={gif} id={idx} /></div>
            })
          }
        </div>
        <footer className="pageButtonsContainer">
          <button onClick={this.handlePages} className="prev">Prev</button>
          <button onClick={this.handlePages} className="next">Next</button>
        </footer>
      </div>
    )
  }
}