Trying to create a list of todos using react-infinite-scroller. The list will display 20 todos, the next todos will be displayed while scrolling. I fetch Todos from 'https://jsonplaceholder.typicode.com/todos'. The fetched todos are saved in the todos variable.
I've modeled this example: https://github.com/CassetteRocks/react-infinite-scroller/blob/master/docs/src/index.js.
Demo here: https://cassetterocks.github.io/react-infinite-scroller/demo/.
I can not see Loading.. appearing and fetching further tasks.
Code here: https://stackblitz.com/edit/react-tcm9o2?file=index.js
import InfiniteScroll from 'react-infinite-scroller';
import qwest from 'qwest';
class App extends Component {
constructor (props) {
super(props);
this.state = {
todos: [],
hasMoreTodos: true,
nextHref: null
}
}
loadTodos(page){
var self = this;
const url = 'https://jsonplaceholder.typicode.com/todos';
if(this.state.nextHref) {
url = this.state.nextHref;
}
qwest.get(url, {
linked_partitioning: 1,
page_size: 10
}, {
cache: true
})
.then((xhr, resp) => {
if(resp) {
var todos = self.state.todos;
resp.map((todo) => {
todos.push(todo);
});
if(resp.next_href) {
self.setState({
todos: todos,
nextHref: resp.next_href
});
} else {
self.setState({
hasMoreTodos: false
});
}
}
});
}
render () {
const loader = <div className="loader">Loading ...</div>;
console.log(this.state.todos);
var items = [];
this.state.todos.map((todo, i) => {
items.push(
<div className="track" key={todo.id}>
<p className="title">{todo.title}</p>
</div>
);
});
return (
<InfiniteScroll
pageStart={0}
loadMore={this.loadTodos.bind(this)}
hasMore={this.state.hasMoreTodos}
loader={loader}>
<div className="tracks">
{items}
</div>
</InfiniteScroll>
)
}
}
UPDATED MY QUESTION
How can I set these places commented in the code?
/*if(this.state.nextHref) {
url = this.state.nextHref;
}*/
and
/*if(resp.next_href) {
self.setState({
todos: todos,
nextHref: resp.next_href
});*/
How can I set the page to change from 1 to 2, next from 2 to 3?
class App extends Component {
constructor (props) {
super(props);
this.state = {
todos: [],
hasMoreTodos: true,
page: 1
}
}
loadTodos(page){
var self = this;
const url = `/api/v1/project/tasks?expand=todos&filter%5Btype%5D=400&${page}&${per_page}`;
/*if(this.state.nextHref) {
url = this.state.nextHref;
}*/
axios.get(url, {
'page': 1,
'per-page': 10
})
.then(resp => {
if(resp) {
var todos = [self.state.todos, ...resp];
/*if(resp.next_href) {
self.setState({
todos: todos,
nextHref: resp.next_href
});*/
} else {
self.setState({
hasMoreTodos: false
});
}
}
});
}
render () {
const loader = <div className="loader">Loading ...</div>;
var divStyle = {
'width': '100px';
'height': '300px';
'border': '1px solid black',
'scroll': 'auto'
};
const items = this.state.todos.map((todo, i) =>
<div className="track" key={todo.id}>
<p className="title">{todo.title}</p>
</div>);
return (
<div style={divStyle}>
<InfiniteScroll
pageStart={1}
loadMore={this.loadTodos.bind(this)}
hasMore={this.state.hasMoreTodos}
loader={loader}>
<div className="tracks">
{items}
</div>
</InfiniteScroll>
</div>
)
}
}
UPDATED 2
Code here: https://stackblitz.com/edit/react-4lwucm
import InfiniteScroll from 'react-infinite-scroller';
import axios from 'axios';
class App extends Component {
constructor() {
super();
this.state = {
todos: [],
hasMoreTodos: true,
page: 1,
nextPage: 1,
finishedLoading: false
};
}
loadTodos = (id) => {
if(!this.state.finishedLoading) {
const params = {
id: '1234',
page: this.state.nextPage,
'per-page': 10,
}
axios({
url: `/api/v1/project/tasks`,
method: "GET",
params
})
.then(res => {
let {todos, nextPage} = this.state;
if(resp) { //resp or resp.data ????
this.setState({
todos: [...todos, ...resp], //resp or resp.data ????
nextPage: nextPage + 1,
finishedLoading: resp.length > 0 //resp or resp.data ????
});
}
})
.catch(error => {
console.log(error);
})
}
}
render() {
const loader = <div className="loader">Loading ...</div>;
const divStyle = {
'height': '300px',
'overflow': 'auto',
'width': '200px',
'border': '1px solid black'
}
const items = this.state.todos.map((todo, i) =>
<div className="track" key={todo.id}>
<p className="title">{todo.title}</p>
</div>);
return (
<div style={divStyle} ref={(ref) => this.scrollParentRef = ref}>
<div>
<InfiniteScroll
pageStart={0}
loadMore={this.loadTodos}
hasMore={true || false}
loader={<div className="loader" key={0}>Loading ...</div>}
useWindow={false}
getScrollParent={() => this.scrollParentRef}
>
{items}
</InfiniteScroll>
</div>
</div>
);
}
}
EDIT: I just realized you used this as a starting point:
Why you get everything at once
Well, because your source is just a big JSON.
The logic in the script you're using is adapted to SoundCloud's API, which paginates (aka. returns chunk-by-chunk) the enormous list.
Your API doesn't paginate, hence why you're receiving everything at once.
See an example result from the SoundCloud API:
{
"collection": [
{
"kind": "track",
"id": 613085994,
"created_at": "2019/04/29 13:25:27 +0000",
"user_id": 316489116,
"duration": 476281,
[...]
},
[...]
],
"next_href": "https://api.soundcloud.com/users/8665091/favorites?client_id=caf73ef1e709f839664ab82bef40fa96&cursor=1550315585694796&linked_partitioning=1&page_size=10"
}
The URL in next_href gives the URL of the next batch of items (the URL is the same but the 'cursor' parameter is different each time). That's what makes the infinite list work: each time, it gets the next batch.
You have to implement server-side pagination to achieve this result.
In response to your edit:
UPDATED MY QUESTION
How can I set these places commented in the code?
/*if(this.state.nextHref) {
url = this.state.nextHref; }*/
and
/*if(resp.next_href) { self.setState({
todos: todos,
nextHref: resp.next_href });*/
Forget about loading the nextHref, you don't have that. What you have is the current page number (up to which page you have loaded the contents).
Try something along the lines of:
if (!self.state.finishedLoading) {
qwest.get("https://your-api.org/json", {
page: self.state.nextPage,
'per-page': 10,
}, {
cache: true
})
.then((xhr, resp) => {
let {todos, nextPage} = self.state;
if(resp) {
self.setState({
todos: [...todos, ...resp],
nextPage: nextPage + 1,
finishedLoading: resp.length > 0, // stop loading when you don't get any more results
});
}
});
}
A few nitpicks
Here you are mutating self.state (bad), in addition to using Array.map with a side-effect (and for that side-effect only) :
var todos = self.state.todos;
resp.map((todo) => {
todos.push(todo);
});
You could write it as:
var todos = [self.state.todos, ...resp];
Same usage of map here:
var items = [];
this.state.todos.map((todo, i) => {
items.push(
<div className="track" key={todo.id}>
<p className="title">{todo.title}</p>
</div>
);
});
Should be written:
const items = this.state.todos.map((todo, i) =>
<div className="track" key={todo.id}>
<p className="title">{todo.title}</p>
</div>);
Related
I have a data in a table and i want to add a pagination option to it. In the user interface, user should be able to choose data per page and page index that's why i added some handle methods to it and try to render it but i faced with the above error.
Whole jsx file is too long to share therefore i only shared codes which seemed important to me and replaced other codes with ... if you need anything else you can ask it in the comments.
function Activity(props) {
...
const variables = useMemo(() => ({
projectId,
language,
env: environment,
pageSize: 20,
filter,
...getSortFunction(),
}), [projectId, language, environment, filter, getSortFunction()]);
const {
data, hasNextPage, loading, loadMore, refetch,
} = useActivity(variables);
//pagination section
const [pagination, setPagination] = useState({
currentPage: 1,
dataPerPage: 10,
indexOfLastData: 9,
indexOfFirstData: 0,
})
const [totalPages, setTotalPages] = useState(1);
const [paginationString, setPaginationString] = useState(`Current page ${pagination.currentPage}, total number of data ${pagination.dataPerPage}`)
const handlePagination = (page, dataPerPage, currentPagination) => {
if (currentPagination.currentPage != page) handlePaginationCurrentPage(page, currentPagination);
if (currentPagination.dataPerPage != dataPerPage) handlePaginationDataPerPage(dataPerPage, currentPagination);
const dataToBeUsed = [...data].slice(pagination.indexOfFirstData, pagination.indexOfLastData);
setTotalPages(Math.ceil(3 / pagination.dataPerPage));
return dataToBeUsed;
}
const handlePaginationString = () => {
setPaginationString(`current page ${pagination.currentPage}, total number of data ${pagination.dataPerPage}`)
}
const handlePaginationCurrentPage = (page, pagination) => {
useEffect(() => {
setPagination({
...pagination,
currentPage: Number(page),
indexOfLastData: pagination.dataPerPage * Number(page) - 1,
indexOfFirstData: pagination.dataPerPage * Number(page) - pagination.dataPerPage
})
})
}
const handlePaginationDataPerPage = (dataPerPage, pagination) => {
useEffect(() => {
setPagination({
...pagination,
dataPerPage: dataPerPage,
indexOfLastData: dataPerPage * pagination.currentPage - 1,
indexOfFirstData: dataPerPage * pagination.currentPage - pagination.dataPerPage
})
})
}
const resetPagination = (e) => {
e.stopPropagation();
handlePagination(1, 10, pagination);
};
...
const renderActions = row => (
<ActivityActionsColumn
outdated={ isUtteranceOutdated(row.datum) }
datum={ row.datum }
handleSetValidated={ handleSetValidated }
onDelete={ handleDelete }
onMarkOoS={ handleMarkOoS }
data={ handlePagination(1, 10, pagination) }
getSmartTips={ utterance => getSmartTips({
nluThreshold, endTime, examples, utterance,
}) }
/>
);
...
const columns = [
{ key: '_id', selectionKey: true, hidden: true },
{
key: 'confidence',
style: { width: '51px', minWidth: '51px' },
render: renderConfidence,
},
{
key: 'intent',
style: { width: '180px', minWidth: '180px', overflow: 'hidden' },
render: renderIntent,
},
{
key: 'conversation-popup', style: { width: '30px', minWidth: '30px' }, render: renderConvPopup,
},
{
key: 'text',
style: { width: '100%' },
render: renderExample,
},
...(can('incoming:w', projectId) ? [
{
key: 'actions',
style: { width: '110px' },
render: renderActions,
},
] : []),
];
const renderTopBar = () => (
<div className='side-by-side wrap' style={ { marginBottom: '10px' } }>
...
<Accordion className='pagination-accordion'>
<Accordion.Title
active={ activeAccordion }
onClick={ () => handleAccordionClick() }
data-cy='toggle-pagination'
className='pagination-accordian-title'
>
<Icon name='dropdown' />
<span className='toggle-pagination'>
{ activeAccordion
? `Hide Pagination Options `
: `Show Pagination Options ` }
</span>
<span className="toggle-pagination pagination-string">
{ activeAccordion
? `${paginationString}`
: `${paginationString}` }
</span>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */ }
<span
data-cy='reset-pagination'
onClick={ e => resetPagination(e) }
role='button'
tabIndex='0'
className='reset-button'
>
<Icon name='redo' size='small' /> Reset
</span>
</Accordion.Title>
</Accordion>
</div>
);
return (
<>
{ !!openConvPopup && <ConversationSidePanel utterance={ openConvPopup } onClose={ () => setOpenConvPopup(false) } /> }
{ renderTopBar() }
{ data && data.length ? (
<>
<DataTable
ref={ tableRef }
columns={ columns }
data={ handlePagination(1, 10, pagination) }
hasNextPage={ hasNextPage }
loadMore={ loading ? () => { } : loadMore }
onScroll={ handleScroll }
selection={ selection }
onChangeSelection={ (newSelection) => {
setSelection(newSelection);
setOpenConvPopup(false);
} }
/>
)}
I can't use data in pagination beacuse it is used in so many places and in those places everything designed assumed data is in its full length so i should use it seperately (ex./ in handlePagination i get it using data.slice() function )
Thanks!
I changed
<DataTable
ref={ tableRef }
columns={ columns }
data={ handlePagination(1, 10, pagination) }
hasNextPage={ hasNextPage }
loadMore={ loading ? () => { } : loadMore }
onScroll={ handleScroll }
selection={ selection }
onChangeSelection={ (newSelection) => {
setSelection(newSelection);
setOpenConvPopup(false);
} }
/>
to
<DataTable
ref={ tableRef }
columns={ columns }
data={ dataToBeShowed }
hasNextPage={ hasNextPage }
loadMore={ loading ? () => { } : loadMore }
onScroll={ handleScroll }
selection={ selection }
onChangeSelection={ (newSelection) => {
setSelection(newSelection);
setOpenConvPopup(false);
} }
/>
and added
const [dataToBeShowed, setDataToBeShowed] = useState(data.slice(pagination.indexOfFirstData, pagination.indexOfLastData));
useEffect(() => {
setDataToBeShowed(data.slice(pagination.indexOfFirstData, pagination.indexOfLastData));
setPaginationString(`Current page ${pagination.currentPage}, total number of data ${pagination.dataPerPage}`)
setTotalPages(Math.ceil(data.length / pagination.dataPerPage));
}, [data, pagination.indexOfFirstData, pagination.indexOfLastData]);
This solved becuase I saw that handlePagination(1, 10, pagination) doesn't render in Activity but in Window which means It can't see the states, therefore i added dataToBeShown and assigned it to real data and since i added useEffect, it follows the change in data (real data).
(if you use onClick vs. you can simply add () => {handleOnBruh()} vs this way it binds itself with the file.)
Thanks!
I found a lot of solutions about this problem but none of them work.
I have a view which renders dynamically components depending on the backend response
/**
* Module dependencies
*/
const React = require('react');
const Head = require('react-declarative-head');
const MY_COMPONENTS = {
text: require('../components/fields/Description'),
initiatives: require('../components/fields/Dropdown'),
vuln: require('../components/fields/Dropdown'),
severities: require('../components/fields/Dropdown'),
};
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class CreateView extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false,
states: props.states,
error: props.error,
spinner: true,
state: props.state,
prevState: '',
components: [],
};
this.handleChange = this.handleChange.bind(this);
this.getRequiredFields = this.getRequiredFields.bind(this);
this.onChangeHandler = this.onChangeHandler.bind(this);
this.changeState = this.changeState.bind(this);
this.loadComponents = this.loadComponents.bind(this);
}
componentDidMount() {
this.loadComponents();
}
onChangeHandler(event, value) {
this.setState((prevState) => {
prevState.prevState = prevState.state;
prevState.state = value;
prevState.spinner = true;
return prevState;
}, () => {
this.getRequiredFields();
});
}
getRequiredFields() {
request.get('/transitions/fields', {
params: {
to: this.state.state,
from: this.state.prevState,
},
})
.then((response) => {
const pComponents = this.state.components.map(c => Object.assign({}, c));
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
this.setState({
components: pComponents,
fields: response.data,
spinner: false,
});
})
.catch(err => err);
}
loadComponents() {
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
}
handleChange(field, value) {
this.setState((prevState) => {
prevState[field] = value;
return prevState;
});
}
changeState(field, value) {
this.setState((prevState) => {
prevState[`${field}`] = value;
return prevState;
});
}
render() {
const Components = this.state.components;
return (
<Page name="CI" state={this.props} Components={Components}>
<Script src="vendor.js" />
<Card className="">
<div className="">
<div className="">
<Spinner
show={this.state.spinner}
/>
{Components.map((component, i) => {
const Comp = component.component;
return (<Comp
key={i}
value={this.state[component.field.name]}
field={component.field}
handleChange={this.handleChange}
modal={this.state.modal}
changeState={this.changeState}
/>);
})
}
</div>
</div>
</div>
</Card>
</Page>
);
}
}
module.exports = CreateView;
and the dropdown component
const React = require('react');
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class DrpDwn extends React.Component {
constructor(props) {
super(props);
this.state = {
field: props.field,
values: [],
};
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('state', this.state.field);
console.log('prevState', prevState.field);
console.log('prevProps', prevProps.field);
console.log('props', this.props.field);
}
render() {
const { show } = this.props.field;
return (show && (
<div className="">
<Dropdown
className=""
onChange={(e, v) => this.props.handleChange(this.props.field.name, v)}
label={this.state.field.name.replace(/^./,
str => str.toUpperCase())}
name={this.state.field.name}
type="form"
value={this.props.value}
width={100}
position
>
{this.state.values.map(value => (<DropdownItem
key={value.id}
value={value.name}
primary={value.name.replace(/^./, str => str.toUpperCase())}
/>))
}
</Dropdown>
</div>
));
}
module.exports = DrpDwn;
The code actually works, it hide or show the components correctly but the thing is that i can't do anything inside componentdidupdate because the prevProps prevState and props are always the same.
I think the problem is that I'm mutating always the same object, but I could not find the way to do it.
What I have to do there is to fill the dropdown item.
Ps: The "real" code works, i adapt it in order to post it here.
React state is supposed to be immutable. Since you're mutating state, you break the ability to tell whether the state has changed. In particular, i think this is the main spot causing your problem:
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
}; return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
You mutate the previous states to changes its components property. Instead, create a new state:
this.setState(prevState => {
const components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return { components }
}
You have an additional place where you're mutating state. I don't know if it's causing your particular problem, but it's worth mentioning anyway:
const pComponents = [].concat(this.state.components);
// const pComponents = [...this.state.components];
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
You do at make a copy of state.components, but this will only be a shallow copy. The array is a new array, but the objects inside the array are the old objects. So when you set ob.field.required, you are mutating the old state as well as the new.
If you want to change properties in the objects, you need to copy those objects at every level you're making a change. The spread syntax is usually the most succinct way to do this:
let pComponents = this.state.components.map(c => {
return {
...c,
field: {
...c.field,
required: 0,
show: false
}
}
});
response.data.forEach(r => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
// Here it's ok to mutate, but only because i already did the copying in the code above
ob.field.required = r.required;
ob.field.show = true;
}
})
I am trying to make filter navigation and want to go back to previous state or trigger function to get the data from another API.
On click of this state, I should be able to clear the filter to return the response from another API.
To understand it completely, please look at the sample App I have created below
Stackblitz : https://stackblitz.com/edit/react-3bpotn
Below is the component
class Playground extends Component {
constructor(props) {
super(props);
this.state = {
selectedLanguage: 'All', // default state
repos: null
};
this.updateLanguage = this.updateLanguage.bind(this);
this.updateLanguagenew = this.updateLanguagenew.bind(this);
}
componentDidMount() {
this.updateLanguage(this.state.selectedLanguage);
}
updateLanguage(lang) {
this.setState({
selectedLanguage: lang,
repos: null
});
fetchPopularRepos(lang).then(
function (repos) {
this.setState(function () {
return { repos: repos };
});
}.bind(this)
);
}
updateLanguagenew(lang) {
if (lang === 'All') {
this.updateLanguage(lang);
return;
}
this.setState({
selectedLanguage: lang,
repos: null
});
fetchPopularReposUpdated(lang).then(
function (repos) {
this.setState(function () {
return { repos: repos };
});
}.bind(this)
);
}
render() {
return (
<div>
<div>
This is the current state : <strong style={{padding: '10px',color:'red'}}>{this.state.selectedLanguage}</strong>
</div>
<div style={{padding: '10px'}}>
On click of above state I should be able to trigger this function <strong>(updateLanguage)</strong> again to clear the filter and load data from this API
</div>
<p>Click the below options</p>
<SelectLanguage
selectedLanguage={this.state.selectedLanguage}
onSelect={this.updateLanguagenew}
/>
{//don't call it until repos are loaded
!this.state.repos ? (
<div>Loading</div>
) : (
<RepoGrid repos={this.state.repos} />
)}
</div>
);
}
}
SelectLanguage component mapping for filter options:
class SelectLanguage extends Component {
constructor(props) {
super(props);
this.state = {
searchInput: '',
};
}
filterItems = () => {
let result = [];
const { searchInput } = this.state;
const languages = [ {
"options": [
{
"catgeory_name": "Sigma",
"category_id": "755"
},
{
"catgeory_name": "Footner",
"category_id": "611"
}
]
}
];
const filterbrandsnew = languages;
let value
if (filterbrandsnew) {
value = filterbrandsnew[0].options.map(({catgeory_name})=>catgeory_name);
console.log (value);
}
const brand = value;
if (searchInput) {
result = this.elementContainsSearchString(searchInput, brand);
} else {
result = brand || [];
}
return result;
}
render() {
const filteredList = this.filterItems();
return (
<div className="filter-options">
<ul className="languages">
{filteredList.map(lang => (
<li
className={lang === this.props.selectedLanguage ? 'selected' : ''}
onClick={this.props.onSelect.bind(null, lang)}
key={lang}
>
{lang}
</li>
))}
</ul>
</div>
);
}
}
Note: This is having the current state {this.state.selectedLanguage}, on click of this I should be able to trigger this function. updateLanguage
The way you are doing set state is not correct
Change
fetchPopularRepos(lang).then(
function (repos) {
this.setState(function () {
return { repos: repos };
});
}.bind(this)
);
To
fetchPopularRepos(lang).then(
function (repos) {
this.setState({
repos: repos
});
}.bind(this)
);
Also Change
fetchPopularReposUpdated(lang).then(
function (repos) {
this.setState(function () {
return { repos: repos };
});
}.bind(this)
);
To
fetchPopularReposUpdated(lang).then(
function (repos) {
this.setState({
repos: repos
});
}.bind(this)
);
I'm using YTS API and I would like to make Infinite scrolling function.
There is a page parameter and limit parameter. It seems it can work with them but I have no idea of how to use it. I'm a beginner user of React. Could you guys help me? Thanks in advance.
fetch('https://yts.am/api/v2/list_movies.json?sort_by=download_count&limit=20')
fetch('https://yts.am/api/v2/list_movies.json?sort_by=download_count&page=2')
This is the link of YTS API https://yts.am/api#list_movies
I would try using React-Waypoint and dispatch an action to fetch the data every time it enters the screen.
The best way IMO is using redux but here's an example without:
state = { currentPage: 0, data: [] };
getNextPage = () => {
fetch(`https://yts.am/api/v2/list_movies.json?sort_by=download_count&page=${this.state.currentPage}`).
then((res) => this.setState((prevState) => ({currentPage: prevState.currentPage + 1, data: res.body}));
}
render(){
<div>
{
this.state.data.map((currentData) => <div>{currentData}</div>)
}
<Waypoint onEnter={this.getNextPage}/>
</div>
}
I would like to show {this._renderList() } infinitely
import React, {Component} from 'react';
import L_MovieList from './L_MovieList';
import L_Ranking from './L_Ranking';
import './L_Right_List.css';
import Waypoint from 'react-waypoint';
class L_BoxOffice extends Component {
state = {
currentPage: 0,
data : []
};
constructor(props) {
super(props);
this.state = {
movies: []
}
this._renderRankings = this._renderRankings.bind(this);
this._renderList = this._renderList.bind(this);
}
componentWillMount() {
this._getMovies();
}
_renderRankings = () => {
const movies = this.state.movies.map((movie, i) => {
console.log(movie)
return <L_Ranking title={movie.title_english} key={movie.id} genres={movie.genres} index={i}/>
})
return movies
}
_renderList = () => {
fetch(`https://yts.am/api/v2/list_movies.json?sort_by=download_count&page=${this.state.currentPage}`)
.then((res) => this.setState((prevState) => ({currentPage: prevState.currentPage + 1, data: res.body}));
const movies = this.state.movies.map((movie) => {
console.log(movie)
return <L_MovieList title={movie.title_english} poster={movie.medium_cover_image} key={movie.id} genres={movie.genres} language={movie.language} runtime={movie.runtime} year={movie.year} rating={movie.rating} likes={movie.likes} trcode={movie.yt_trailer_code}/>
})
return movies
}
_getMovies = async () => {
const movies = await this._callApi()
this.setState({
movies
})
}
_callApi = () => {
return fetch('https://yts.am/api/v2/list_movies.json?sort_by=download_count&limit=10').then(potato => potato.json())
.then(json => json.data.movies)
.catch(err => console.log(err))
}
getNextPage = () => {
fetch(`https://yts.am/api/v2/list_movies.json?sort_by=download_count&page=${this.state.currentPage}`).
then((res) => this.setState((prevState) => ({currentPage: prevState.currentPage + 1, data: res.body}));
}
render() {
const {movies} = this.state;
let sub_title;
let right_information;
if (this.props.page == 'main') {
sub_title = <div>Today Box Office</div>;
right_information = <div>
aaa</div>;
} else if (this.props.page == 'box_office') {
right_information = <div className={movies
? "L_Right_List"
: "L_Right_List--loading"}>
{this._renderList()}
{
this.state.data.map((currentData) => <div>{this._renderList()}</div>)
}
<Waypoint onEnter={this.getNextPage}/>
</div>;
}
return (<div style={{
backgroundColor: '#E5E5E5',
paddingTop: '20px',
paddingLeft: '20px'
}}>
{sub_title}
<div className={movies
? "L_Left_Ranking"
: "L_Left_Ranking--loading"}>
<div className="L_Left_Ranking_title">영화랭킹</div>
{this._renderRankings()}
</div>
{right_information}
</div>);
}
}
export default L_BoxOffice;
I am learning React and I am trying to use a component to show details of a selected item in a table, my problem is that when I click Next in the table (it is paginated), the state got updated and re-render the component and if I do click many times, it enqueue and the details component is changing between all the items I did click on.
I don't know if there is a good practice for doing this kind of things, i tried to search for something like this but nothing yet.
The component I am using (using public API):
import React from 'react';
class Pokemon extends React.Component {
constructor() {
super();
this.state = {
name: "",
abilities: [],
stats: [],
weight: 0,
height: 0,
base_experience: 0,
};
}
fetch_pokemon = () => {
fetch(this.props.pokemon_url)
.then(res => res.json())
.then(res => {
this.setState({
name: res.name,
abilities: res.abilities,
stats: res.stats,
weight: res.weight,
height: res.height,
base_experience: res.base_experience,
});
});
}
render() {
if(this.props.pokemon_url !== ''){
this.fetch_pokemon();
return (
<div>
<h1>{this.state.name}</h1>
Weight: {this.state.weight}<br />
Height: {this.state.height}<br />
Abilities:
<ul>
{this.state.abilities.map(({ability}, index) => {
return (<li key={index}>{ability.name}</li>);
})}
</ul>
</div>
);
}
else{
return (<h3>Choose one pokémon from the list...</h3>);
}
}
}
export default Pokemon;
My main component:
import React, { Component } from 'react';
//import logo from './logo.svg';
import './App.css';
import Pokemon from './Pokemon';
class App extends Component {
constructor() {
const i_pokemons = 25;
super();
this.state = {
pokemons: [],
pokemons_list: [],
pokemon_show_list: [],
p_init: 0,
p_final: i_pokemons,
pagination: i_pokemons,
pages: 0,
status: '',
enable_buttons: false,
current_page: 0,
current_pokemon_url: '',
URL: `https://pokeapi.co/api/v2/pokemon/?limit=99999`
};
}
componentDidMount(){
this.fetch_pokemons();
}
prev(){
this.setState({
current_page: this.state.current_page - 1,
p_init: this.state.p_init - this.state.pagination,
p_final: this.state.p_final - this.state.pagination
}, () => {
this.fetch_new_page();
});
}
next(){
this.setState({
current_page: this.state.current_page + 1,
p_init: this.state.p_init + this.state.pagination,
p_final: this.state.p_final + this.state.pagination
}, () => {
this.fetch_new_page();
});
}
fetch_new_page = () => {
const current_id = (this.state.current_page - 1) * this.state.pagination;
this.setState({
pokemon_show_list: this.state.pokemons_list.slice(current_id, current_id + 25)
});
this.fetch_pokemons();
}
fetch_pokemons = callback => {
this.setState({
status: 'Waiting for the server and retrieving data, please wait...',
enable_buttons: false
});
return new Promise((resolve, reject) => {
fetch(this.state.URL)
.then(res => res.json())
.then(res => {
if(!res.detail){
this.setState({
pokemons: res,
pokemons_list: res.results,
enable_buttons: true,
status: 'Done',
pokemon_show_list: res.results.slice(this.state.p_init, this.state.p_final)
});
if(this.state.pages === 0){
this.setState({
pages: Math.round(this.state.pokemons_list.length / this.state.pagination),
current_page: 1
});
}
resolve(true);
}else{
reject("Error");
this.setState({status: `Error`});
}
})
.catch(error => {
this.setState({status: `Error: ${error}`});
reject(error);
});
});
}
showPokemon({url}){
this.setState({
current_pokemon_url: url
});
}
render() {
console.log("Render");
return(
<div className="general">
<div className="pokemons-info">
{this.state.status !== '' && this.state.status}
<br />
<table className="pokemon-list">
<thead>
<tr>
<th>Name</th>
<th>More info.</th>
</tr>
</thead>
<tbody>
{this.state.pokemon_show_list.map((pokemon, index) => {
return (
<tr className="l" key={index}>
<td>{pokemon.name}</td>
<td><a className="btn btn-secondary" onClick={this.showPokemon.bind(this, pokemon)} href={`#${pokemon.name}`}>More info.</a></td>
</tr>
);
})}
</tbody>
</table>
<button className="btn btn-primary" disabled={this.state.current_page <= 1} onClick={this.prev.bind(this)}>Prev</button>
Page: {this.state.current_page} of {this.state.pages}
<button className="btn btn-primary" disabled={this.state.current_page === this.state.pages} onClick={this.next.bind(this)}>Next</button>
</div>
<Pokemon pokemon_url={this.state.current_pokemon_url}/>
</div>
);
}
}
export default App;
Feel free for giving any advice
I refactored and clean your code a little bit, but I guess the code below aims what you are looking for. (Read more about functional component 'logicless components').
const API = 'https://pokeapi.co/api/v2/pokemon/';
const PAGE_SIZE = 25;
function Status(props) {
return (
<div>{props.value}</div>
)
}
function Pagination(props) {
return (
<div>
<button
onClick={props.onPrevious}
disabled={props.disabled}>
Prev
</button>
<button
onClick={props.onNext}
disabled={props.disabled}>
Next
</button>
</div>
)
}
function Pokemon(props) {
return (
<div>
<h1>{props.pokemon.name}</h1>
Weight: {props.pokemon.weight}<br />
Height: {props.pokemon.height}<br />
Abilities:
<ul>
{props.pokemon.abilities.map(({ability}, index) => {
return (<li key={index}>{ability.name}</li>);
})}
</ul>
</div>
)
}
function PokemonTable (props) {
return (
<table className="pokemon-list">
<thead>
<tr>
<th>Name</th>
<th>More info.</th>
</tr>
</thead>
<tbody>
{props.children}
</tbody>
</table>
);
}
function PokemonRow (props) {
return (
<tr>
<td>{props.pokemon.name}</td>
<td>
<a href="#" onClick={() => props.onInfo(props.pokemon)}>
More info.
</a>
</td>
</tr>
);
}
class App extends React.Component {
state = {
pokemons: [],
detailedPokemons : {},
loading: false,
status : null,
previous : null,
next : null
}
componentDidMount () {
this.getPokemons(`${API}?limit=${PAGE_SIZE}`)
}
request(url) {
return fetch(url)
.then(blob => blob.json());
}
getPokemons (url) {
this.setState(state => ({
...state,
loading : true,
status : 'Fetching pokemons...'
}));
this.request(url)
.then(response => {
console.log(response)
this.setState(state => ({
...state,
previous : response.previous,
next : response.next,
pokemons : response.results,
loading : false,
status : null
}))
})
.catch(err => {
this.setState(state => ({
...state,
loading : false,
status : 'Unable to retrieved pockemons'
}));
});
}
getPokemonDetail (pokemon) {
const { detailedPokemons } = this.state;
const cachePokemon = detailedPokemons[pokemon.name];
if (cachePokemon !== undefined) { return; }
this.setState(state => ({
...state,
loading : true,
status : `Fetching ${pokemon.name} info`
}));
this.request(pokemon.url)
.then(response => {
this.setState(state => ({
...state,
loading: false,
status : null,
detailedPokemons : {
...state.detailedPokemons,
[response.name]: {
name: response.name,
abilities: response.abilities,
stats: response.stats,
weight: response.weight,
height: response.height,
base_experience: response.base_experience
}
}
}))
})
.catch(err => {
console.log(err)
this.setState(state => ({
...state,
loading : false,
status : 'Unable to retrieved pockemons'
}));
});
}
renderPokemons () {
const { pokemons } = this.state;
return pokemons.map(pokemon => (
<PokemonRow
pokemon={pokemon}
onInfo={this.handleView}
/>
));
}
renderDetailPokemons () {
const { detailedPokemons } = this.state;
return (
<ul>
{Object.keys(detailedPokemons).map(pokemonName => (
<li key={pokemonName}>
<Pokemon pokemon={detailedPokemons[pokemonName]}/>
</li>
))}
</ul>
)
}
handleView = (pokemon) => {
this.getPokemonDetail(pokemon);
}
handlePrevious = () => {
const { previous } = this.state;
this.getPokemons(previous);
}
handleNext = () => {
const { next } = this.state;
this.getPokemons(next);
}
render () {
const { loading, detailedPokemons, status, next, previous } = this.state;
return (
<div className='general'>
<div className="pokemons-info">
{ status && <Status value={status} /> }
<PokemonTable>
{this.renderPokemons()}
</PokemonTable>
<Pagination
disabled={loading}
onPrevious={this.handlePrevious}
onNext={this.handleNext}
/>
{
Object.keys(detailedPokemons).length > 0 &&
this.renderDetailPokemons()
}
</div>
</div>
)
}
}
ReactDOM.render(
<App />,
document.querySelector('#app')
);