Executing a loop in React class component - javascript

I'm building a pagination component and I'm struggling to execute a for loop so I can dynamically generate the pages. I initially had a function component, but I want to switch it to a class component so I can manage state in it. (I know, I can use hooks, but Im practicing class components at the moment).
I initially added the for loop in the render method but it is executing the loop twice because the component ir rendering twice. Then, I tried componentDidMount() but it doesn't do anything... then used componentWillMount() and it worked. However, I know this could be bad practice.
Any ideas? See below the component with componentDidMount()
import React, { Component } from 'react';
import styles from './Pagination.module.css';
class Pagination extends Component {
state = {
pageNumbers: [],
selected: '',
};
componentDidMount() {
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
this.state.pageNumbers.push(i);
}
}
classActiveForPagineHandler = (number) => {
this.setState({ selected: number });
};
render() {
return (
<div className={styles.PaginationContainer}>
<nav>
<ul className={styles.PageListHolder}>
{this.state.pageNumbers.map((num) => (
<li key={num}>
<a
href="!#"
className={
this.state.selected === num
? styles.Active
: styles.PageActive
}
onClick={() => {
this.props.paginate(num);
// this.props.classActiveForPagineHandler(num);
}}
>
{num}
</a>
</li>
))}
</ul>
</nav>
</div>
);
}
}
export default Pagination;

You better push all the numbers into array and then update pageNumbers state. this.state.pageNumbers.push(i); does not update state directly, you need use setState after your calculation completes.
componentDidMount() {
const { pageNumbers = [] } = this.state
const { totalDogs, dogsPerPage } = this.props
for (let i = 1; i <= Math.ceil(totalDogs / dogsPerPage); i++) {
pageNumbers.push(i);
}
this.setState({ pageNumbers })
}
Demo link here

you should not update state like this :
this.state.pageNumbers.push(i);
do this:
this.setState((s) => {
return {
...s,
pageNumbers: [...s.pageNumbers, i]
}
})

Do not mutate state directly in react component. Use setState for all updates.
componentDidMount() {
const pageNumbers = [];
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
pageNumbers.push(i);
}
this.setState({ pageNumbers });
}
Alternatively, you can simplify the code using Array.from for this case.
componentDidMount() {
this.setState({
pageNumbers: Array.from(
{ length: Math.ceil(this.props.totalDogs / this.props.dogsPerPage) },
(_, i) => i + 1
),
});
}

Related

Trying to get a counter to work with React and multiple components

I am working on trying to get this counter for pintsLeft to work. This is my first project with React and I feel that I am either not passing the property of the array correctly or my function code is not set correctly.
^^^^KegDetail.js^^^^
import React from "react";
import PropTypes from "prop-types";
function KegDetail(props){
const { keg, onClickingDelete} = props
return (
<React.Fragment>
<hr/>
<h2>{keg.name} Made By {keg.brewery}</h2>
<p>abv {keg.abv}</p>
<h3>price {keg.price}</h3>
<p>{keg.pintsLeft} total pints left</p> {/* Make this a percentage */}
<hr/>
<button onClick={ props.onClickingEdit }>Update Keg</button>
<button onClick={()=> onClickingDelete(keg.id) }>Delete Keg</button>
<button onClick={()=> this.onSellingPint()}>Sell A Pint!</button>
</React.Fragment>
);
}
KegDetail.propTypes = {
keg: PropTypes.object,
onClickingDelete: PropTypes.func,
onClickingEdit:PropTypes.func,
onSellingPint:PropTypes.func
}
export default KegDetail;
That was my KegDetail.js
import React, {useState} from "react";
import NewKegForm from "./NewKegForm";
import DraftList from "./DraftList";
import KegDetail from "./KegDetail";
import EditKegForm from "./EditKegForm";
class DraftControl extends React.Component {
constructor(props){
super(props);
this.state = {
kegFormVisibleOnPage: false,
fullDraftList: [],
selectedKeg: null,
editing: false,
pints: 127,
};
this.handleClick = this.handleClick.bind(this);
this.handleSellingPint = this.handleSellingPint.bind(this);
}
handleClick = () => {
if (this.state.selectedKeg != null){
this.setState({
kegFormVisibleOnPage: false,
selectedKeg: null,
editing: false
});
} else {
this.setState(prevState => ({
kegFormVisibleOnPage: !prevState.kegFormVisibleOnPage,
}));
}
}
handleSellingPint = () => {
this.setState({
pints:this.state.pints-1
})
};
render() {
let currentlyVisibleState = null;
let buttonText = null;
if (this.state.editing){
currentlyVisibleState = <EditKegForm keg = {this.state.selectedKeg} onEditKeg = {this.handleEditingKegInDraftList} />
buttonText = "Return to the Draft List"
}
else if (this.state.selectedKeg != null){
currentlyVisibleState = <KegDetail keg = {this.state.selectedKeg} onClickingDelete = {this.handleDeletingKeg}
onClickingEdit = {this.handleEditClick} onSellingPint = {this.handleSellingPint}/>
buttonText = "Return to the Keg List"
My DraftControl.js code
I don't know what I am doing wrong. I cant get the keg.pintsLeft to pass a number when I console.log, So I may be targeting it incorrectly.
Thanks again!
Try it like this:
handleSellingPint = () => {
this.setState(prevState => {
return {
pints: prevState.pints-1
}
})
};
edit
Also, you invoke the onSellingPint() in a wrong way.
It's not a class component, so React doesn't know what does this refer to.
The function itself is passed in as a prop, so you should reference it like this: <button onClick={() => props.onSellingPint() />
handleSellingPint = (id) => {
const clonedArray = [...this.state.fullDraftList]
for (let i = 0; i < this.state.fullDraftList.length; i++){
if (clonedArray[i].id === id){
clonedArray[i].pintsLeft -= 1
}
}
this.setState({
fullDraftList: clone
});
}
Is what I came up with.
Since you are alteriting a state within an array, you need to clone the array and work on that array, not the "real" one.
Thanks for all your help!

mapping state to props is returning undefined and does not update

i am mapping my store state to my pagination component but it's returning undefined and only running one time when it's value is undefined not when it has value and i'm setting it to state and it's not updating the state too so i had to use store.subscribe method but this is not very good and how can i set the value to slicedPage outside the for loop so it won't run as many time as total_pages if i set outside the for loop it's empty and hope i explained it well
import React from "react";
import { connect } from "react-redux";
import store from "../../store";
import { all_Movies } from "../../actions";
My Pagination Class
export class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = {
one: 0,
last: 10,
total_pages: this.props.pages,
pageNumber: [],
slicedPage: []
};
store.subscribe(() => {
if (store.getState().all_Movies.total_pages) {
console.log(store.getState().all_Movies.total_pages);
this.setState(
{ total_pages: store.getState().all_Movies.total_pages },
() => {
for (let i = 1; i < this.state.total_pages; i++) {
this.setState(
prevState => ({ pageNumber: [...prevState.pageNumber, i] }),
() => {
// page.push(this.state.pageNumber[i])
// console.log(page);
this.setState({
one:
this.state.pageNumber.slice(
this.state.one,
this.state.last
).length -
this.state.pageNumber.slice(
this.state.one,
this.state.last
).length
});
this.setState({
last: this.state.pageNumber.slice(
this.state.one,
this.state.last
).length
});
I want to set this outside the for Loop
this.setState({
slicedPage: this.state.pageNumber.slice(
this.state.one,
this.state.last
)
});
----------^^^^^^
console.log(this.state.one, this.state.last);
}
);
}
}
);
}
});
}
render() {
return (
<div className="pagination">
<ul className="pagination_wrapper">
{this.state.slicedPage.map(number => (
<li key={number} className="pagination_link">
<button
onClick={() => {
this.loadPage(number);
}}
>
{number}
</button>
</li>
))}
</ul>
</div>
);
}
}
Mapping state to props
const mapStateToProps = state => ({
pages: state.all_Movies.total_pages,
type: state.all_Movies.type
});
export default connect(
mapStateToProps,
{ all_Movies }
)(Pagination);

Toggle Class in if else statement-React

I am trying to toggle a class in React (only in the else statement).
class Inf extends React.Component {
constructor() {
super();
this.state = {
pizzaData: data
}
}
renderList(info){
const list = this.state.pizzaData.map((entry, index) => {
if (entry.occupied==true){
return <li class="coloring" key={index}>Seat: {entry.seat}{entry.row}</li>;
}
else{
return <li class="colored" key={index}>Seat: {entry.seat}{entry.row}</li>;
}
});
return(
<ul>{list}</ul>
)
}
Now, looking over some of the documentation I was unsure how to do this. I know that there needs to be a "toggle" on the li and (I think) something like this below the this.state={:
pizzaData:data
},
handleClick function(
But I am not sure.
I created a simple example of how you can update your code, also with two components (similar to the idea by #THEtheChad), but without using context since according to react docs it is discouraged to use context directly if you want your app to be stable. If state and props management in app gets too complicated you can include redux (which internally also uses context), but for now I am not including redux since it be might over-complication in this simple case.
Here is PizzaList which has pizzas on its state. The component will render PizzaItem components and pass a callback down so that each PizzaItem can notify its parent (PizzaList) when it is clicked. PizzaList has the responsibility of toggling PizzaItem when it is clicked.
class PizzaList extends React.PureComponent {
state = {
pizzas: []
}
componentDidMount() {
// fetch data about pizzas via an API and perform this.setState
this.setState({ pizzas: [{ seat: 20, occupied: false }, { seat: 10, occupied: true }, { seat: 30, occupied: true }] });
}
handlePizzaItemClick = (pizzaInd) => {
this.setState((prevState) => {
// find clicked pizza and toggle its occupied property
const pizzas = prevState.pizzas.map((pizza, ind) => {
if (ind === pizzaInd)
return { ...pizza, ...{ occupied: !pizza.occupied } };
return pizza;
});
return { pizzas: pizzas };
});
}
render () {
return (
<ul>
{this.state.pizzas.map((pizza, index) =>
<PizzaItem
onClick={this.handlePizzaItemClick}
index={index}
pizza={pizza}
/>)}
</ul>
);
}
}
PizzaItem is a simple function component that doesn't have any state.
const PizzaItem = ({ index, pizza, onClick }) => {
const { seat, row, occupied } = pizza;
const pizzaClassName = occupied ? 'coloring' : 'colored';
return (
<li key={index}
className={pizzaClassName}
onClick={() => onClick(index)}>
Seat: {seat} {row}
</li>
);
}
Here is a working example on codesandbox.
I would update your code and split it into two components, a list component and an item component (in this case pizza?). The list component would provide a method for modifying the list using the context API. In my example, I have an updatePizza method that I pass down in the context.
Then, in the child component, you have a click handler that updates the occupied status of the pizza and tells the parent what the new status is using the context method.
This makes sure that the parent component always has the current state for all the pizzas and passes that down to the children. The parent component becomes the single source of truth here.
class List extends React.Component {
static childContextTypes = {
updatePizza: React.PropTypes.func
}
constructor({ pizzas }){
super()
this.state = { pizzas }
}
updatePizza = (idx, pizza) => {
this.setState( ({ pizzas }) => {
pizzas[idx] = pizza;
return { pizzas }
})
}
getChildContext() {
return { updatePizza: this.updatePizza }
}
render(){
return <ul>{this.state.pizzas.map((pizza, idx) => <Pizza key={ idx } { ...pizza }>)}<ul>
}
}
class Pizza extends React.Component {
static contextTypes = {
updatePizza: React.PropTypes.func
}
handleClick = () => {
this.state.occupied = !this.state.occupied;
this.context.updatePizza(this.state.key, this.state)
}
render() {
const { key, seat, row, occupied } = this.state;
const status = occupied ? 'coloring' : 'colored';
return <li key={ key } className={ status } onClick={ handleClick }> Seat: { seat } { row }</li>
}
}

Maximum call stack size exceeded - Connected React Component

I can't for the life of me figure out why I'm getting error:
Maximum call stack size exceeded
When this code is run. If I comment out:
const tabs = this.getTabs(breakpoints, panels, selectedTab);
the error goes away. I have even commented out other setState() calls to try and narrow down where the problem was at.
Code (removed the extra functions):
export default class SearchTabs extends Component {
constructor() {
super();
this.state = {
filters: null,
filter: null,
isDropdownOpen: false,
selectedFilter: null,
};
this.getTabs = this.getTabs.bind(this);
this.tabChanged = this.tabChanged.bind(this);
this.setSelectedFilter = this.setSelectedFilter.bind(this);
this.closeDropdown = this.closeDropdown.bind(this);
this.openDropdown = this.openDropdown.bind(this);
}
componentDidMount() {
const { panels } = this.props;
if (!panels || !panels.members || panels.members.length === 0) {
this.props.fetchSearch();
}
}
getTabs(breakpoints, panels, selectedTab) {
const tabs = panels.member.map((panel, idx) => {
const { id: panelId, headline } = panel;
const url = getHeaderLogo(panel, 50);
const item = url ? <img src={url} alt={headline} /> : headline;
const classname = classNames([
searchResultsTheme.tabItem,
(idx === selectedTab) ? searchResultsTheme.active : null,
]);
this.setState({ filter: this.renderFilters(
panel,
breakpoints,
this.setSelectedFilter,
this.state.selectedFilter,
this.state.isDropdownOpen,
) || null });
return (
<TabItem
key={panelId}
classname={`${classname} search-tab`}
headline={headline}
idx={idx}
content={item}
onclick={this.tabChanged(idx, headline)}
/>
);
});
return tabs;
}
render() {
const { panels, selectedTab } = this.props;
if (!panels || panels.length === 0) return null;
const tabs = this.getTabs(breakpoints, panels, selectedTab);
return (
<div className={searchResultsTheme.filters}>
<ul className={`${searchResultsTheme.tabs} ft-search-tabs`}>{tabs}</ul>
<div className={searchResultsTheme.dropdown}>{this.state.filter}</div>
</div>
);
}
}
export const TabItem = ({ classname, content, onclick, key }) => (
<li key={key} className={`${classname} tab-item`} onClick={onclick} >{content}</li>
);
Because of this loop:
render -----> getTabs -----> setState -----
^ |
| |
|____________________________________________v
You are calling getTabs method from render, and doing setState inside that, setState will trigger re-rendering, again getTabs ..... Infinite loop.
Remove setState from getTabs method, it will work.
Another issue is here:
onclick={this.tabChanged(idx, headline)}
We need to assign a function to onClick event, we don't need to call it, but here you are calling that method, use this:
onclick={() => this.tabChanged(idx, headline)}

Infinite scroll for React List

So I have a list of 5k elements. I want to display them in parts, say each part is 30 items. The list of items is in the component's state. Each item is an object taken from the API. It has properties on which I have to make an API call. By parts, to avoid enormous load time. So this is what I've got so far(simplified):
let page=1;
class GitHubLists extends Component {
constructor(props) {
super(props);
this.state = {
repos: [],
contributors: []
}
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
axios.get(org)
.then(res => setState({contributors: res})
}
handleScroll() {
page++;
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
render() {
const contributors = this.state.contributors.slice(0,30*page).map(contributor =>
<li key={contributor.id}>{contributor.login} {contributor.contributions}<a href={contributor.url}>View on GitHub</a></li>
);
return (
<div onScroll={this.handleScroll}>{contributors}</div>
)
}
}
Like I said each item(contributor in this case) has properties which values are links for the API calls. 3 to be exact. On each one of them, I need to make an API call, count the items inside the response and display them.
You can use react-virtualized (6.8k stars), it has been designed for this purpose.
Here is an official example with a list of 1000 elements or here with a Infinite Loader.
I wrote an easier live example here where you can modify code.
For your problem, you need to do your API calls in the rowRenderer and play with the overscanRowCount to prefetch rows. (docs of the List component)
I've made a simple pagination adapted from another GIST that I've already used that makes total sense for your purpose, you just need to implement your code.
class ItemsApp extends React.Component {
constructor() {
super();
this.state = {
items: ['a','b','c','d','e','f','g','h','i','j','k','2','4','1','343','34','a','b','c','d','e','f','g','h','i','j','k','2','4','1','343','34','a','b','c','d','e','f','g','h','i','j','k','2','4','1','343','34','33'],
currentPage: 1,
itemsPerPage: 30
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
this.setState({
currentPage: Number(event.target.id)
});
}
render() {
const { items, currentPage, itemsPerPage } = this.state;
// Logic for displaying current items
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = items.slice(indexOfFirstItem, indexOfLastItem);
const renderItems = currentItems.map((item, index) => {
return <li key={index}>{item}</li>;
});
// Logic for displaying page numbers
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(items.length / itemsPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map(number => {
return (
<li
key={number}
id={number}
onClick={this.handleClick}
>
{number}
</li>
);
});
return (
<div>
<ul>
{renderItems}
</ul>
<ul id="page-numbers">
{renderPageNumbers}
</ul>
</div>
);
}
}
ReactDOM.render(
<ItemsApp />,
document.getElementById('app')
);
https://codepen.io/anon/pen/jLZjQZ?editors=0110
Basically, you should insert your fetched array inside the items state, and change the itemsPerPage value according to your needs, I've set 30 occurrences per page.
I hope it helps =)
Ok, there is definitely something wrong about how I wrote my app. It is not waiting for all API calls to finish. It sets the state (and pushes to contributors) multiple times. This is the full code:
let unorderedContributors = [];
let contributors = [];
class GitHubLists extends Component {
constructor(props) {
super(props);
this.state = {
repos: [],
contributors: [],
currentPage: 1,
itemsPerPage: 30,
isLoaded: false
};
this.handleClick = this.handleClick.bind(this)
}
componentWillMount() {
//get github organization
axios.get(GitHubOrganization)
.then(res => {
let numberRepos = res.data.public_repos;
let pages = Math.ceil(numberRepos/100);
for(let page = 1; page <= pages; page++) {
//get all repos of the organization
axios.get(`https://api.github.com/orgs/angular/repos?page=${page}&per_page=100&${API_KEY}`)
.then(res => {
for(let i = 0; i < res.data.length; i++) {
this.setState((prevState) => ({
repos: prevState.repos.concat([res.data[i]])
}));
}
})
.then(() => {
//get all contributors for each repo
this.state.repos.map(repo =>
axios.get(`${repo.contributors_url}?per_page=100&${API_KEY}`)
.then(res => {
if(!res.headers.link) {
unorderedContributors.push(res.data);
}
//if there are more pages, paginate through them
else {
for(let page = 1; page <= 5; page++) { //5 pages because of GitHub restrictions - can be done recursively checking if res.headers.link.includes('rel="next"')
axios.get(`${repo.contributors_url}?page=${page}&per_page=100&${API_KEY}`)
.then(res => unorderedContributors.push(res.data));
}
}
})
//make new sorted array with useful data
.then(() => {contributors =
_.chain(unorderedContributors)
.flattenDeep()
.groupBy('id')
.map((group, id) => ({
id: parseInt(id, 10),
login: _.first(group).login,
contributions: _.sumBy(group, 'contributions'),
followers_url: _.first(group).followers_url,
repos_url: _.first(group).repos_url,
gists_url: _.first(group).gists_url,
avatar: _.first(group).avatar_url,
url: _.first(group).html_url
}))
.orderBy(['contributions'],['desc'])
.filter((item) => !isNaN(item.id))
.value()})
.then(() =>
this.setState({contributors, isLoaded: true})
)
)
})
}
})
}
handleClick(event) {
this.setState({currentPage: Number(event.target.id)})
}
render() {
const { contributors, currentPage, contributorsPerPage } = this.state;
//Logic for displaying current contributors
const indexOfLastContributor = currentPage * contributorsPerPage;
const indexOfFirstContributor = indexOfLastContributor - contributorsPerPage;
const currentContributors = contributors.slice(indexOfFirstContributor, indexOfLastContributor);
const renderContributors = currentContributors.map((contributor, index) => {
return <li key={index}>{contributor}</li>;
});
//Logic for displaying page numbers
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(contributors.length / contributorsPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map(number => {
return (
<li
key={number}
id={number}
onClick={this.handleClick}
>
{number}
</li>
);
});
return (
<div>
<ul>
{renderContributors}
</ul>
<ul id="page-numbers">
{renderPageNumbers}
</ul>
</div>
);
}
}
How can I fix it so the state is set once and then I can render contributors from the state (and making API calls with values of the properties: followers_url, repos_url and gists_url)?

Categories

Resources