How to implement pagination on frontend in ReactJS? - javascript

I have made cricket web app using ReactJS, NodeJS, MongoDB. On frontend I am displaying data from JSON objects per page I want to display 16 JSON objects. I am using .slice(0,16) method to slice JSON objects from REST API which returns 577 objects. Now how can I implement pagination for page 2,3,4,5.
For page 2 -> slice(17,33)
For page 3 -> slice(34,49)
and so on...
Below is my code and screenshot of my webapp:
in content.js :
import React, { Component } from 'react';
import './content.css';
class Content extends Component {
constructor(props){
super(props);
this.state = {
matches:[],
loading:true
};
}
componentDidMount(){
fetch('api/matches')
.then(res => res.json())
.then(res => {
console.log(res)
this.setState({
matches:res.slice(0,16),
loading:false
})
})
}
renderMatches() {
return this.state.matches.map(match => {
return (
<div class="col-lg-3">
<div id="content">
<p class="match">MATCH {match.id}</p>
<h4>{match.team1}</h4>
<p>VS</p>
<h4>{match.team2}</h4>
<div class="winner">
<h3>WINNER</h3>
<h4>{match.winner}</h4>
</div>
<div class="stats">
<button type="button" class="btn btn-success">View Stats</button>
</div>
</div>
</div>
);
})
}
render() {
if (this.state.loading) {
return <div>>Loading...</div>
}
return (
<div>
<div class="row">
{this.renderMatches()}
</div>
</div>
);
}
}
export default Content;
In Pagination.js :
import React, { Component } from 'react';
class Pagination extends Component {
render() {
return (
<div>
<div class="container">
<h2>Pagination</h2>
<p>The .pagination class provides pagination links:</p>
<ul class="pagination">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
</div>
</div>
);
}
}
export default Pagination;
Screenshot for more clarity :
enter image description here

How about store all the matches in the component state and only slice during rendering, based on the page query string?
In componentDidMount(), after fetching the data, simply set the state with all the matches:
this.setState({
matches: res,
})
Then, given you want to control the pagination with a query string in the url, like:
?page=2
You can filter the matches in render():
const RECORDS_PER_PAGE = 16;
const page = parseInt(getPageNumber(), 10) || 1; // defaults to 1 if no page query specified
const offset = (page - 1) * RECORDS_PER_PAGE
// do your mapping (do this in your renderMapping() method)
const matchComponents = this.state.matches.slice(0 + offset, RECORDS_PER_PAGE + offset).map(/* ... */);
where getPageNumber() is a method that gives you the query string value for page:
// Use your own query string parser if you wish
getPageNumber() {
return window.location.search // <-- gives you access to the query parameters
.slice(1) // remove the `?`
.split("&")
.reduce((queryHash, queryParam) => {
// e.g. ?page=3 becomes ["page", "3"]
const query = queryParam.split('=');
return Object.assign({}, queryHash, ({
[query[0]]: query[1] || null
}));
}, {}).page;
}

Related

How to change pagination variables in react

I am working on building pagination. I'm still working on my API to fetch posts based on pagination, but at the moment I am stuck on a problem with states.
In my main file (where the posts will be), my code looks like this:
// Import modules
import React from "react";
import axios from "axios";
import url from "url";
// Import components
import { Snippet } from "../Snippet";
import { CreatePost } from "./CreatePost";
import { Pagination } from "./Pagination";
// Import styles
import "./css/Content.css";
// Axios Init
const api = axios.create({
baseURL: `http://localhost:5000/api/`,
});
export class Content extends React.Component {
state = {
collections: [
{ title: "ReactJS", color: "red" },
{ title: "HTML", color: "cyan" },
{ title: "CSS", color: "pink" },
],
snippets: [],
limitSnippets: 3,
page: 0,
};
constructor(props) {
super(props);
}
componentDidMount() {
this.getSnippets();
}
getSnippets = async () => {
try {
let data = await api
.get(
`/snippets/fetchAll?limitSnippets=${this.state.limitSnippets}&page=${this.state.page}`,
{
body: {
limitSnippets: 3,
page: 1,
},
}
)
.then(({ data }) => data);
this.setState({ snippets: data });
} catch (error) {
console.log(error);
}
};
updatePagination = (page) => {
this.state.page = page;
console.log(this.state.page);
};
render() {
return (
<div className="content">
<h1 className="content-header">Snippets</h1>
<CreatePost contentUpdater={this.getSnippets} />
<Pagination updatePagination={this.updatePagination} />
<div className="w-layout-grid grid">
{this.state.snippets.map((snippet, i) => {
return (
<Snippet
key={i}
title={snippet.title}
code={snippet.code}
updatedDate={snippet.updatedDate}
createdDate={snippet.createdDate}
language={snippet.language}
creator={snippet.creator}
collections={snippet.collections}
/>
);
})}
</div>
<Pagination />
</div>
);
}
}
export default Content;
In pagination file, my code looks like this:
export const Pagination = (props) => {
// States
const [page, setPage] = useState(0);
// Axios Init
const api = axios.create({
baseURL: `http://localhost:5000/api/`,
});
const handleLeft = (event) => {
event.preventDefault();
if (page > 0) {
setPage(page - 1);
props.updatePagination(page);
} else {
console.log("handleLeft(): page not > 0");
}
//props.updatePagination(page);
//}
};
const handleRight = (event) => {
event.preventDefault();
// page < fetchAllPages
setPage(page + 1);
props.updatePagination(page);
};
/*useEffect(() => {
props.updatePagination(page);
}, [page]);
*/
return (
<div className="paginate-div">
<div className="paginate-next">
<div className="paginate-next-icon" onClick={handleLeft}>
<i className="fas fa-caret-left"></i>
</div>
</div>
<a href="#" className="paginate-button first w-inline-block">
<div className="paginate-text">1</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">2</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">3</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">4</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">5</div>
</a>
<a href="#" className="paginate-button w-inline-block">
<div className="paginate-text">6</div>
</a>
<a href="#" className="paginate-button last w-inline-block">
<div className="paginate-text">...</div>
</a>
<div className="paginate-next" onClick={handleRight}>
<div className="paginate-next-icon">
<i className="fas fa-caret-right"></i>
</div>
</div>
</div>
);
};
I have my pagination component which is passed a prop that's a function to updatePagination(). The pagination component has functions for left and right button for switching thru pagination, and when it is clicked, the main file gets the pagination updated.
The problem I am having (sorry if it is confusing by the way i worded this)
The default page is 0 (which is basically page 1).
The crazy thing is when I press right (handleRight is called on submit), it stays at page 0, then if I click it again it goes to 1, then after if I press the left button (which called handleLeft on submit) while it is on page 1, it goes up to 2 somehow, but if I click it again it goes back down to 1, then if I click again it goes to 0.
Why is this strange problem occurring?
setPage is asynchronous, so when you setPage to decrement and then immediately call props.updatePage, props.updatePage is receiving the old value of page. You can read all about this common problem here.
const handleRight = (event) => {
event.preventDefault();
// Hey React, set page to page + 1 and rerender the component
setPage(page + 1);
// But before you do that, call props.updatePagination on the old value
props.updatePagination(page);
};
You should ask yourself, though, why you even store two stateful values of page at all (one in the parent component, one in the child component). Couldn't the Content component keep track of your page state (as it already does) and pass it down as a prop, getting rid of your need for a page state in the child component? This is called Lifting State Up, and it's a fundamental concept in React, which incentivizes you to use the least amount of state possible to avoid exactly this kind of desync. Furthermore, from the code you've shared, the Pagination component just displays the page numbers - why does it even need to be stateful at all?
The problem was that the old value of page was being used in updatePagination(), I fixed this by not running updatePagination(page) in the same place, I used useEffect(), and checked for changes to {page}, and ran updatePagination in there.
useEffect(() => {
props.updatePagination(page);
}, [page]);
The handleLeft, handleRight functions were changed to look like this:
const handleLeft = (event) => {
event.preventDefault();
let newPage = page - 1;
if (page > 0) {
setPage(newPage);
} else {
console.log("handleLeft(): page not > 0");
}
//}
};
NOTE"
In the comments section, someone made a point that I should not be storing the page number in two places, but rather store them in one place and pass it over as props. I have not currently tried to do this, but I plan on doing this soon. But for now, this is the best answer for this scenario.

Accessing JSON from public folder in ReactApp

I have a a JSON file which I would like to use its content into my React App.
This is an excerpt from my code
export default class App extends Component{
constructor(props){
super(props);
this.state = {
entry : []
}
}
componentDidMount(){
fetch(process.env.PUBLIC_URL + `./js/data.json`)
.then(res => res.json())
.then(json => this.setState({ entry: json }));
}
render(){
return(
<div>
<ul>
{this.state.entry.map( x => (
<li>
<div className='centering'>
<span>
<img alt='' key={x.img} src={process.env.PUBLIC_URL + `./img/${x.img}.jpg`}/>
</span>
<span className='txt'>
{ x.date }
</span>
<span>
<p>{ x.ctx }</p>
</span>
</div>
<div className='hr-line'></div>
</li>
))}
</ul>
</div>
)
}
}
this is the content of my data.json
{
"entry" : {
"2" : [
{
"date":"1/1/1",
"img":"profile",
"ctx":"as"
}
],
"1" : [
{
"date":"1/1/1",
"img":"profile",
"ctx":"as"
}
]
}
When I save the file, on the browser it shows TypeError: this.state is null
onrender {this.state.entry.map (x => ...
Is there something missing, or it is impossible to do so?
Thank you in advance.
Rendering is done before componentDidmount. You need to wait for your data to load before to do your map. Something like :
export default class App extends Component{
constructor(props){
super(props);
this.state = {
entry : [],
anyData: false
}
}
componentDidMount(){
fetch(process.env.PUBLIC_URL + `./js/data.json`)
.then(res => res.json())
.then(json => this.setState( prevState => ({ ...prevState, entry: json, anyData: true })));
}
render(){
if(!anyData) {
return <Loader />
}
return(
<div>
<ul>
{this.state.entry.map( x => (...))}
</ul>
</div>
)
}
}
There is also a possibility to use async lifecycle method :
async componentDidMount() {
const res = await fetch(...);
const json = await res.json();
this.setState( prevState => ({ ...prevState, entry: json, anyData: true }));
}
But it does not change the fact that it will be done after rendering.
React expects the .map for iterative rendering to work on an array of items only.
Therefore, changing the json input to return an array of items will help resolve the problem.
Existing input that does not work with .map:
"entry" : { "1" : [....], "2" : [....] } --> Input is not an array of items.
--> .map does not support this format
How to make it work with .map?
"entry" : [ {"1" : {....} , {"2" : {....} ] --> Input is an array of items.
--> .map works with this format
Additional: How to avoid 'null value' error in render method?
The render method must explicitly check for null condition before trying to render elements. This can be done as follows:
render(){
return(
<div>
<ul>
{ (this.state.entry) &&
this.state.entry.map( x => (
<li>
...
</li>
))
}
</ul>
</div>
)
}

Axios Multiple requests to return different object values

I am building a Film grid that return their Id, thumbnail, title, episode number and released date.
How can I display the Object Values on each specific view of species.names?
getSpecies() it returns object value but how can i passed them in render method?
DetailFilms.js Component:
import React, { Component } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
class DetailFilms extends Component {
state = {
film: null,
specie: null,
}
getFilms() {
let id = this.props.match.params.films_id;
return axios.get('https://swapi.co/api/films/' + id)
.then(res => {
this.setState({
film: res.data
})
// console.log(res)
})
}
getSpecies() {
let id = this.props.match.params.films_id;
return axios.get('https://swapi.co/api/species/' + id)
.then((res) => {
const species = res.data;
const keys = Object.keys(species)
console.log(species)
this.setState({
species: res.data.results,
keys: keys
})
})
}
componentDidMount() {
this.getFilms();
this.getSpecies();
}
render() {
const film = this.state.film ? (
<div className="jumbotron">
<h2 className="display-4">{this.state.film.title}</h2>
<p className="lead">{this.state.film.director}</p>
<hr className="my-4" />
<p>{this.state.film.producer}</p>
<p>Episode NÂș: {this.state.film.episode_id}</p>
<p className="font-italic">{this.state.film.opening_crawl}</p>
<p>Species: {this.state.film.species.name}</p>
<Link className="btn btn-primary btn-lg" to="/" role="button">Character</Link>
</div>
) : (
<div className="spinner-border" role="status">
<span className="sr-only">Loading...</span>
</div>
)
return (
<div className="d-flex justify-content-center align-items-center ">
{film}
</div>
)
}
}
export default DetailFilms;
CodeSandbox Demo & Api Documentation Swapi
The props you are passing in with the route, it is called films_id not species_id.
So in your getSpecies method, this is how you should get the id of the current movie:
let id = this.props.match.params.films_id;
Then if you see the species api: https://swapi.co/api/species/
if you query it through the id, you will get the specie corresponding to that id, which is not the species of that movie.
In order to get all the species of that movie, you need to do an HTTP get request to the root https://swapi.co/api/species/ and then check all the results which have as film the one for which you are seeing the details. These will be all the species of that movie.

How to fix 'Objects are not valid as a React child'

Codesandbox link.
I'm getting this error when trying to use filter() through a big array of objects (defined as 'filteredCharacters'), and render only those match the id of '6' to the screen (only one does).
I console.log(filteredCharacters), and I can clearly see in console that it works. But for some reason, I'm getting a "Objects are not valid as a React child" error thrown.
The code below is from /components/content.js, in the Codesandbox link above.
import React, { Component } from 'react';
import Intro from '../intro/intro';
class Content extends Component {
render() {
// Grab the 'characters' object from App.js, and assign it to 'this.props'
const { characters } = this.props;
// Filter the chracters and return only whose 'id' belongs to that of '6'
const filteredCharacters = characters.filter(characters => {
if (characters.id === 6) {
return (
<div className="characters" key={characters.id}>
<p>Name: {characters.Name}</p>
<p>ID: {characters.id}</p>
<p>Job: {characters.Job}</p>
<p>Age: {characters.Age}</p>
<p>Weapon: {characters.Weapon}</p>
<p>Height: {characters.Height}</p>
<p>Birthdate: {characters.Birthdate}</p>
<p>Birthplace: {characters.Birthplace}</p>
<p>Bloodtype: {characters.Bloodtype}</p>
<p>Description: {characters.Description}</p>
</div>
)
}
});
// Check to see if it logs properly (it does)
console.log(filteredCharacters);
// When trying to render this to the screen below, it doesn't work
return (
<div>
{filteredCharacters}
</div>
)
}
}
export default Content;
filter will only create a new array with all the elements that returned a truthy value from the function.
You can instead use filter first to get the relevant characters, and then use map on the new array to get the JSX you want to render.
const filteredCharacters = characters
.filter(character => character.id === 6)
.map(character => (
<div className="characters" key={character.id}>
<p>Name: {character.Name}</p>
<p>ID: {character.id}</p>
<p>Job: {character.Job}</p>
<p>Age: {character.Age}</p>
<p>Weapon: {character.Weapon}</p>
<p>Height: {character.Height}</p>
<p>Birthdate: {character.Birthdate}</p>
<p>Birthplace: {character.Birthplace}</p>
<p>Bloodtype: {character.Bloodtype}</p>
<p>Description: {character.Description}</p>
</div>
));
Adding to #Tholle's answer, you could combine those operations into one with reduce
const filteredCharacters = characters
.reduce((acc, character) => {
if (character.id !== 6) return acc;
acc.push(<div className="characters" key={character.id}>
<p>Name: {character.Name}</p>
<p>ID: {character.id}</p>
<p>Job: {character.Job}</p>
<p>Age: {character.Age}</p>
<p>Weapon: {character.Weapon}</p>
<p>Height: {character.Height}</p>
<p>Birthdate: {character.Birthdate}</p>
<p>Birthplace: {character.Birthplace}</p>
<p>Bloodtype: {character.Bloodtype}</p>
<p>Description: {character.Description}</p>
</div>);
return acc;
}, []);
Currently you are using one simple object to behave as a node in the HTML structure of the component you are writing. One of the best practices to use react in such cases is to create and then call this as a react component itself.
Following is your code that now has a separate component that can be called on need:
import React, { Component } from 'react';
import Intro from '../intro/intro';
const FilteredCharcters = characters => {
characters.filter(character => {
if (character.id === 6) {
return (
<div className="characters" key={character.id}>
<p>Name: {character.Name}</p>
<p>ID: {character.id}</p>
<p>Job: {character.Job}</p>
<p>Age: {character.Age}</p>
<p>Weapon: {character.Weapon}</p>
<p>Height: {character.Height}</p>
<p>Birthdate: {character.Birthdate}</p>
<p>Birthplace: {character.Birthplace}</p>
<p>Bloodtype: {character.Bloodtype}</p>
<p>Description: {character.Description}</p>
</div>
)
}
});
class Content extends Component {
render() {
const { characters } = this.props;
return (
<div>
<FilteredCharacters characters={characters} />
</div>
)
}
}
export default Content;

How can I grab the key of a list item generated from a map function?

So I am learning React, and I've tried searching for solutions to my problem both on stackoverflow and on React's own documentation, but I am still stumped.
Essentially, I have a list of 10 subreddits that is being mapped to list items in the form of the subredditsArray variable.
I render the results, and try to pass the selected item when I click that list item to my getSubredditInfo function. However, this doesn't work - event.target.key is undefined. (To clarify, I am looking to grab the key of the single list element that I have clicked).
When I try to just get event.target, I get the actual htmlElement (ex: <li>Dota2</li>), where as I want to get the key, or at least this value into a string somehow without the tags. I also tried putting my onClick method in the list tag of the map function, but that did not work.
Here is the relevant code:
//this is where I get my data
componentDidMount(){
fetch('https://www.reddit.com/api/search_reddit_names.json?query=dota2')
.then(results => {
return results.json();
})
.then(redditNames => {
//this is there I set my subreddits state variable to the array of strings
this.setState({subreddits: redditNames.names});
})
}
getSubredditInfo(event){
//console.log(event.target.key); <-- DOESNT WORK
}
render() {
var subredditsArray = this.state.subreddits.map(function(subreddit){
return (<li key={subreddit.toString()}>{subreddit}</li>);
});
return (
<div className="redditResults">
<h1>Top 10 subreddits for that topic</h1>
<ul onClick={this.getSubredditInfo}>{subredditsArray}</ul>
</div>
);
}
My questions essentially boil down to:
How do I grab the key value from my list object?
Additionally, is there a better way to generate the list than I currently am?
Thank you in advance.
EDIT: Added my componentDidMount function in hopes it clarifies things a bit more.
try the following code:
class App extends React.Component {
constructor(props){
super(props);
this.state = {subreddits:[]};
}
componentDidMount(){
fetch('https://www.reddit.com/api/search_reddit_names.json?query=dota2')
.then(results => {
return results.json();
})
.then(redditNames => {
//this is there I set my subreddits state variable to the array of strings
this.setState({subreddits: redditNames.names});
})
}
getSubredditInfo(subreddit){
console.log(subreddit);
}
render() {
return <div className="redditResults">
<h1>Top 10 subreddits for that topic</h1>
<ul>
{
this.state.subreddits.map((subreddit)=>{
return (<li key={subreddit.toString()} onClick={()=>this.getSubredditInfo(subreddit)}>{subreddit}</li>);
})
}
</ul>
</div>;
}
}
ReactDOM.render(
<App/>,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
please check the onClick event handler now. its an arrow function and its calling the getSubredditInfo function with your subreddit now. so you will get it there.
so its basically different way of calling the handler to pass data to the handler.
it works as you expect it to.
You can use lamda function or make component for item list which have own value for getSubredditInfo function
getSubredditInfo(value) {}
render() {
var subredditsArray = this.state
.subreddits.map((subreddit, i) =>
(<li key={i}
onClick={() => this.getSubredditInfo(subreddit)}>{subreddit}</li>));
return (
<div className="redditResults">
<h1>Top 10 subreddits for that topic</h1>
<ul>{subredditsArray}</ul>
</div>
);
}
1) Key should be grabbed either by the id in your object in array. Or you can combine the 2 properties to create a unique key for react to handle re-renders in a better way.
If you have a string array, you may use a combination of string value + index to create a unique value, although using index is not encouraged.
Given a quick example for both below.
2) A better way could be to move your map function into another function and call that function in render function, which will return the required JSX. It will clean your render function.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
subredditsObjArray: [
{ id: 1, value: 'A'},
{ id: 2, value: 'B'},
{ id: 3, value: 'C'},
{ id: 4, value: 'D'}
],
subredditsArray: ['A', 'B', 'C', 'D'],
selectedValue: ''
};
}
getSubredditInfo = (subreddit) => {
console.log(subreddit)
this.setState({
selectedValue: ((subreddit && subreddit.id) ? subreddit.value : subreddit),
});
}
render() {
return (
<div className="redditResults">
<p>Selected Value: {this.state.selectedValue}</p>
<h1>Top {this.state.subredditsArray.length || '0'} subreddits for that topic</h1>
<p>With Objects Array</p>
<ul>
{
this.state.subredditsObjArray
&& this.state.subredditsObjArray.map(redditObj => {
return (<li key={redditObj.id}><button onClick={() => this.getSubredditInfo(redditObj)}>{redditObj.value || 'Not Found'}</button></li>);
})
}
</ul>
<br />
<p>With Strings Array</p>
<ul>
{
this.state.subredditsArray
&& this.state.subredditsArray.map((reddit, index) => {
return (<li key={reddit + '-' + index}><button onClick={() => this.getSubredditInfo(reddit)}>{reddit || 'Not Found'}</button></li>);
})
}
</ul>
</div>
);
}
}
ReactDOM.render(
<App etext="Edit" stext="Save" />,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
Are you trying to do this? I'm not sure what you want to do.
getSubredditInfo(e, subreddit) {
console.log(subreddit)
}
render() {
const { subreddits } = this.state
var subredditsArray = subreddits.map(subreddit => (
<li
key={subreddit.toString()}
onClick={(e) => {
this.getSubredditInfo(e, subreddit)
}}
>
{subreddit}
</li>
))
return (
<div className="redditResults">
<h1>Top 10 subreddits for that topic</h1>
<ul>{subredditsArray}</ul>
</div>
);
}
The key purpose is to pass your subreddit to the onClick function so you will receive the value while you click the item.
If you still get error try this and tell me what's happened.
render() {
const { subreddits } = this.state
var subredditsArray = subreddits.map(subreddit => (
<li
key={subreddit.toString()}
onClick={(e) => {
console.log(subreddit.toString())
}}
>
{subreddit}
</li>
))
return (
<div className="redditResults">
<h1>Top 10 subreddits for that topic</h1>
<ul>{subredditsArray}</ul>
</div>
);
}

Categories

Resources