Robert Jones
Robert Jones

Reputation: 73

React- How can I change the state when I click on a different page?

In my program, I have a variety of cards that are put on screen with a picture of a character and information about said character. I am pulling all of this information from an API, and am tasked with doing a client side pagination to display only a few of the cards on screen at a time.

Here is my code:

genCard = () => {

const { isLoaded, items, currentPage, totalNumberPages, recordsPerPage } = this.state;
if (isLoaded) {
 let returnCard = [];
 let i;
 for(i = currentPage; i < recordsPerPage; i++) {
  returnCard.push(<Card key={items[i].id} cardName={items[i].name} imgSrc={items[i].image} birthYear={items[i].birth_year}/>);

 }
 return returnCard;
}
return null;

};

  handlePageClick = (data) => {
    let selected = data.selected;
    let offset = Math.ceil(selected * this.props.perPage);
this.setState({

})

};

As you can see, I'm using a for loop to display only 10 items (cards) on the screen at a time. What I'm trying to do is when you click on another page, I want it to re-render and display the other cards on screen.

So, how can I make this happen? How can I set the state to the page that you click on, so that it renders the correct cards onto the screen?

Thanks in advance for your help. Hopefully that made sense.

Upvotes: 1

Views: 2694

Answers (3)

Sai Sandeep Vaddi
Sai Sandeep Vaddi

Reputation: 290

UPDATE: Adding code snippet here in addition to JSFiddle link below.

function Card(props) {

	return <div>I'm card {props.id}</div>
}

class Cards extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
    	isLoaded: false,
      items: [1,2,3,4,5,6,7,8,9,10, 11, 12, 13, 14, 15],
      currentPage: 1,
      recordsPerPage: 5,
      itemStartIndexOnPage: 0,
      tempCount: 0
    }
  }
  
  getCards = () => {
    const {items, itemStartIndexOnPage, recordsPerPage} = this.state;
    const itemsForThisPage =  items.slice(itemStartIndexOnPage, itemStartIndexOnPage + recordsPerPage);
    let cards = itemsForThisPage.map((item, i) => <Card key={item} id={item} {...item} />)
    return cards;
  }
  
  handlePageClick = (data) => {
  	let selected = data;
    let offset = Math.ceil((selected - 1) * this.state.recordsPerPage);
    this.setState({
    	currentPage: selected,
      itemStartIndexOnPage: offset
    })
  }
  
  render() {
    return (
      <div>
       Page No: {this.state.currentPage}
       {this.getCards()}
       <button onClick={() => this.handlePageClick(1)}>Page 1</button>
       <button onClick={() => this.handlePageClick(2)}>Page 2</button>
       <button onClick={() => this.handlePageClick(3)}>Page 3</button>
      </div>
    )
  }
}

ReactDOM.render(<Cards />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>

I've wrote this snippet if that's what you are looking for.

setState re-renders component tree. So call handlePageClick on clicking pagination buttons and you can call getCards() within render() function which updates the cards in the component.

If you need to get subset of items from API end point on page-click you can make handlePageClick asynchronous and call setState after await or in then. If you are getting all the data at once, you can do that in componentDidMount and store in state.

Upvotes: 1

Icepickle
Icepickle

Reputation: 12796

Well, let's be honest, you seem to be able to work with react already enough as you can access the state, and you are rendering some items as children, so what is stopping you currently from defining an onClick handler that will call a class defined function (either with class properties you seemingly use) or by binding the method in the constructor?

Is it just the fact that you are missing a pager, so you can actually delegate the events?

I think Sai Sandeep Vaddis answer gives you already some information on how to deal with paged data, the only thing I want to add, is how you could structure over several components in order to keep it reusable throughout your application.

Now, keep in mind, I don't have a clue about your grander design, and the code you have shown (except for your loop which doesn't really start correct) seems to be working

const { Component } = React;

// overly basic pager
const Pager = ({ page, pageSize, itemCount, onPageChange }) => {
  const totalPages = Math.ceil( itemCount / pageSize );
  const children = [];
  for (let i = 0; i < totalPages; i++) {
    children.push( <span key={ i } onClick={ () => onPageChange(i) } className={ classNames( 'page-item', { selected: i === page } ) }>{ i + 1 }</span> );
  }
  return <div className="pager">{ children }</div>;
};

// a component to keep a local state (current page state, and all the items it holds)
class PagedList extends Component {
  constructor() {
    super();
    this.state = {
      page: 0
    };
    this.changePage = this.changePage.bind( this );
  }
  changePage( targetPage ) {
    this.setState( { page: targetPage } );
    // this could theoretically inform any parent that the page has changed
    // in case this should be tracked from the parent (in which case having
    // the page as state doesn't really make sense anymore, it was just a 
    // quick way to enable paging for already retrieved data
  }
  render() {
    const { page = 0 } = this.state;
    // some default settings on this.props (can be done with getDerivedProps or getDefaultProps as well)
    const { items, pageSize = 10, component = props => JSON.stringify(props) } = this.props;
    // no items yet, wait for the next render I suppose
    if (!items) {
      return <div>No items available</div>;
    }
    // render pager / current page items / pager
    return <div>
      <Pager page={ page } pageSize={ pageSize } itemCount={ items.length } onPageChange={ this.changePage } />
      { items.slice( page * pageSize, (page * pageSize) + pageSize ).map( item => React.createElement( component, {...item, key: item.id } ) ) }
      <Pager page={ page } pageSize={ pageSize } itemCount={ items.length } onPageChange={ this.changePage } />
    </div>
  }
}

// very basic component displaying the name of the breed
const BreedItem = ({ name }) => <div>{ name }</div>;

// component that will load a random image when originally loaded
// could probably use a flag that says if it's still mounted before updating the state
// but, it's simply good to be aware of it :)
class BreedCard extends Component {
  constructor() {
    super();
    this.state = {
    };
  }
  componentDidMount() {
    const { name } = this.props;
    fetch( `https://dog.ceo/api/breed/${name}/images/random` )
      .then( response => response.json() )
      .then( data => this.setState( { image: data.message } ) );
  }
  render() {
    const { name } = this.props;
    const { image } = this.state;
    return <div className="card">
      <BreedItem name={ name } />
      { image ? <img src={ image } alt={ name } /> : <span>loading image</span> }
    </div>;
  }
}

// gets the items from the api (all at once to be fair)
// and then renders them in a PagedList
// doesn't really have a clue about what page it in, just decides on
// - pageSize
// - data
// - component to render
class BreedList extends Component {
  constructor() {
    super();
    this.state = {
      items: []
    };
  }
  componentDidMount() {
    fetch( 'https://dog.ceo/api/breeds/list/all' )
      .then( response => response.json() )
      .then( data => this.setState( { items: Object.keys( data.message ).map( key => ({ id: key, name: key }) ) } ) );
  }
  render() {
    const { items } = this.state;
    
    return <PagedList pageSize={10} items={items} component={ BreedCard } />;
  }
}

// lets get cracking then :)
const container = document.querySelector('#container');
ReactDOM.render( <BreedList />, container );
.page-item {
  display: inline-block;
  padding: 5px;
  cursor: pointer;
  border-radius: 5px;
}
.page-item:hover {
  background: silver;
}
.selected {
  background-color: blue;
  color: white;
}
.pager {
  border: solid black 1px;
  display: flex;
  flex-direction: row;
  font-size: .8em;
}
.card {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  border: solid #a0a0a0 1px;
  margin: 3px;
  padding: 5px;
}
.card > img {
  max-width: 320px;
  max-height: 200px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script id="classnames" src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.js"></script>
<div id="container"></div>

Upvotes: 0

antonini
antonini

Reputation: 121

You can add the following in handlePageClick

this.setState({ currentPage: selected })

and change the genCard as below

 const startIndex = currentPage * recordsPerPage
 for(i = startIndex i < startIndex + recordsPerPage; i++) {
  returnCard.push(<Card key={items[i].id} cardName={items[i].name} 
  imgSrc={items[i].image} birthYear={items[i].birth_year}/>);
 }

Upvotes: 0

Related Questions