Consuming Paginated API in React Component - javascript

I'm just getting started with React. As a simple exercise, I wanted to create some components for viewing data retrieved from the JsonMonk API. The API contains 83 user records and serves them in pages of 10.
I am trying to develop a component for viewing a list of users one page at a time which I called UserList. The code for it is below:
class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
pageNumber: 1,
users: [],
};
this.onPageNext = this.onPageNext.bind(this);
}
componentDidMount() {
this.fetchUsers(this.state.pageNumber)
.then((users) => this.setState({users: users}));
}
async fetchUsers(pageNumber) {
const response = await fetch(`https://jsonmonk.com/api/v1/users?page=${pageNumber}`);
const jsonResponse = await response.json();
return jsonResponse.data.records;
}
onPageNext() {
// ...
}
render() {
const postElements = this.state.users.map(
(props) => <User key={props._id} {...props} />);
return (
<div>
{postElements}
<div>
<button onClick={this.onPageNext}>Next</button>
</div>
</div>
);
}
}
The problem I am having pertains to the onPageNext method of my component. When the user clicks the "Next" button, I want to make a fetch for the next page of data and update the list.
My first attempt used an asynchronous arrow function passed to setState like so:
onPageNext() {
this.setState(async (state, props) => {
const nextPageNumber = state.pageNumber + 1;
const users = await this.fetchUsers(nextPageNumber);
return {pageNumber: nextPageNumber, users: users}
})
}
However, it does not seem React supports this behavior because the state is never updated.
Next, I tried to use promise .then syntax like so:
onPageNext() {
const nextPageNumber = this.state.pageNumber + 1;
this.fetchUsers(nextPageNumber)
.then((users) => this.setState({pageNumber: nextPageNumber, users: users}));
}
This works but the problem here is that I am accessing the class's state directly and not through setState's argument so I may receive an incorrect value. Say the user clicks the "Next" button three times quickly, they may not advance three pages.
I have essentially run into a chicken-or-the-egg type problem. I need to pass a callback to setState but I need to know the next page ID to fetch the data which requires calling setState. After studying the docs, I feel like the solution is moving the fetch logic out of the UsersList component, but I'm not entirely sure how to attack it.
As always, any help is appreciated.

You need to change onPageNext as below:
onPageNext() {
this.setState( prevState => {
return {pageNumber: prevState.pageNumber + 1}
}, () =>{
this.fetchUsers(this.state.pageNumber).then(users => this.setState({users: users}) )
});
}
Here is the Complete Code:
import React from "react";
export default class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
pageNumber: 1,
users: [],
};
this.onPageNext = this.onPageNext.bind(this);
}
componentDidMount() {
this.fetchUsers(this.state.pageNumber)
.then((users) => {
console.log(users, 'users');
this.setState({users: users})
}
);
}
async fetchUsers(pageNumber) {
const response = await fetch(`https://jsonmonk.com/api/v1/users?page=${pageNumber}`);
const jsonResponse = await response.json();
return jsonResponse.data.records;
}
onPageNext() {
this.setState( prevState => {
return {pageNumber: prevState.pageNumber + 1}
}, () =>{
this.fetchUsers(this.state.pageNumber).then(users => this.setState({users: users}) )
});
}
render() {
const postElements = this.state.users.map(
(user) => <User key={user._id} {...user} />);
return (
<div>
{postElements}
<div>
<button onClick={this.onPageNext}>Next</button>
</div>
</div>
);
}
}
function User(props) {
return (
<div>
<div style={{padding: 5}}>Name: {props.first_name} {props.last_name}</div>
<div style={{padding: 5}}>Email: {props.email}</div>
<div style={{padding: 5}}>Phone: {props.mobile_no}</div>
<hr/>
</div>
);
}
Here is the Code Sandbox

Related

Catch error films.map is not a function in React

I get the catch error or type error on
{films.map((film, i) => { ...
can not read proper 'map' or undefined from the Rest Api swapi in React.
build id url to show information
Create the component
State to add api data
Binding the function fetchData to this state
Call to api function characters
Call to api function movies
Life circle to call the function
Rendering the component
Bring the data of the character to the data to show them
mapping the list
const api_url_films = 'https://swapi.dev/api/films/'
class Card extends Component {
constructor(props) {
super(props)
this.state = {
films: []
}
this.fetchDataFilms = this.fetchDataFilms.bind(this)
}
fetchDataFilms(){
fetch(api_url_films)
.then(data => (data.json()))
.then(results => {
const api_data = results
this.setState({
films: api_data
})
})
}
componentDidMount() {
this.fetchDataFilms()
}
render() {
const {films} = this.state
return(
<div>
{films.map((film, i) => {
return (
<li key={i}>{film}</li>
)
}
)}
</div>
)
}
}
I try to add in a unordered list also but doesn't work
thanks
I think html is rendered before data is fetched although the films variable is an empty array in initial state.
When films variable is set from fetching function, html will be re-rendered.
try this code. This component will show an empty box until film data is fetched.
hope this works!
render() {
let films = this.state.films
if(films == null || films == []) return <></>
return(
<div>
{films.map((film, i) => {
return (
<li key={i}>{film}</li>
)
}
)}
</div>
)
}
The response has this shape:
{
count: number,
next: any,
previous: any,
results: Object[], // <-- this is what you want
}
Update the handler to access into the response object and save the results array.
fetchDataFilms(){
fetch(api_url_films)
.then(data => (data.json()))
.then(results => {
const api_data = results
this.setState({
films: api_data.results, // <-- access the results array
})
})
}
Additionally, the film objects you are mapping are still object, so they are not valid React children. You will need to access specific properties.
{films.map((film) => {
return (
<li key={film.title}>{film.title}</li> // or director, or producer, etc...
)
}

Lifting up the state to the main component in React application using hooks

I am learning reactjs and trying to implement small things for practice. The idea is simple, to add records (feedbackTasks) to the database and list these records (when first time page is loaded and later when a new record is added). Please see the image below.
The main component is ManageFeedbackTasks. I keep the list of the feedbackTask items in its state (st_feedbackTaskList). The update of this list is performed through add_to_st_feedbackTask function. If the first time this list is generated, all the fetched data (coming from PrintFeedbackTasks component) is set to the st_feedbackTaskList. If not, only the added item (coming from ShowAddingFeedbackTaskForm) is inserted in the list.
export function ManageFeedbackTasks() {
const [st_feedbackTaskList, setFeedbackTaskList] = useState([]);
const add_to_st_feedbackTask = (data) => {
if (st_feedbackTaskList.length == 0) {
setFeedbackTaskList(data);
} else {
const { id, title, description } = data;
setFeedbackTaskList([...st_feedbackTaskList, { id, title, description }]);
}
}
return (
<>
<ShowAddingFeedbackTaskForm onAddingItem={add_to_st_feedbackTask} />
<PrintFeedbackTasks onListingFeedbackTasks={add_to_st_feedbackTask} feedbackTasks={st_feedbackTaskList} />
</>
);
}
Below is the PrintFeedbackTasks function. This function receives the feedbackTasks list from the main component ManageFeedbackTasks. This list is first time fetched from the database using fetchFeedbackTasks. Inside fetchFeedbackTasks, props.onListingFeedbackTasks(response.data) sends the fetched list back to the main component to update the state (st_feedbackTaskList).
const PrintFeedbackTasks = (props) => {
const [st_isInitialized, setInitialized] = useState(false);
const fetchFeedbackTasks = () => {
axios.get('api/FeedbackTask/Index')
.then(response => props.onListingFeedbackTasks(response.data))
.catch(error => console.log(error));
}
useEffect(() => {
if (!st_isInitialized) {
fetchFeedbackTasks();
}
setInitialized(true);
});
return (
<React.Fragment>
{
props.feedbackTasks.map(taskItem =>....
}
</React.Fragment>
);
}
The component below displays the Add form and handles the form submission. When a new item is added, this new item is again sent back to the main component using props.onAddingItem.
const ShowAddingFeedbackTaskForm = (props) => {
const [st_title, setTitle] = useState('');
const [st_description, setDescription] = useState('');
const handleSubmit = async (event) => {
event.preventDefault();
await axios(...)
.then(function (response) {
setTitle('');
setDescription('');
//This will update the list of the feedback task in the main component
props.onAddingItem({
id: response.data,
title: st_title,
description: st_description
});
//GET THE ID HERE
console.log(response.data);
}).catch(function (error) {
console.log(error);
});
}
return (
<form onSubmit={handleSubmit}>
<input
placeholder="Title..."
type="text"
value={st_title}
onChange={(event) => setTitle(event.target.value)}
/>
<input
placeholder="Description..."
type="text"
value={st_description}
onChange={(event) => setDescription(event.target.value)}
/>
<button>Add Feedback Task</button>
</form>
);
}
I wonder if this way of lifting and managing the state is robust. Any suggestions to improve the code? Also, I wonder if I should put these components into their own pages (for example, one page or adding a record and another one for listing). Would this make more sense in the react world?
The idea to lift the state up to the parent is correct. However due to your code structure you could be causing a lot of re-renders and a few performance optimizations can be made in your solution. One more thing is that instead of fetching feedbackTasks in PrintFeedbackTasks component you should do it in the parent itself. Also useEffect takes a second parameter which you can use to execute it on initial mount
You can use useCallback hook to memoize functions too.
ManageFeedbackTasks
export function ManageFeedbackTasks() {
const [st_feedbackTaskList, setFeedbackTaskList] = useState([]);
const fetchFeedbackTasks = useCallback(() => {
axios.get('api/FeedbackTask/Index')
.then(response => props.onListingFeedbackTasks(response.data))
.catch(error => console.log(error));
}, []);
useEffect(() => {
fetchFeedbackTasks();
}, []);
const add_to_st_feedbackTask = useCallback((data) => {
setFeedbackTaskList(prevTaskList => {
if (prevTaskList.length == 0) {
return data;
} else {
const { id, title, description } = data;
return [...prevTaskList, { id, title, description }];
}
});
}, [])
return (
<>
<ShowAddingFeedbackTaskForm onAddingItem={add_to_st_feedbackTask} />
<PrintFeedbackTasks onListingFeedbackTasks={add_to_st_feedbackTask} feedbackTasks={st_feedbackTaskList} />
</>
);
}
PrintFeedbackTasks
const PrintFeedbackTasks = (props) => {
return (
<React.Fragment>
{
props.feedbackTasks.map(taskItem =>....
}
</React.Fragment>
);
}
As far as the idea to split show and update TaskList is concerned it as product decision which can be made depending on how long is the list of fields that the user needs to fill at once

API taking too long, map function firing before data loads

import React, { Component } from 'react';
import {withProvider} from './TProvider'
import ThreeCardMap from './ThreeCardMap';
class Threecard extends Component {
constructor() {
super();
this.state = {
newlist: []
}
}
componentDidMount(){
this.props.getList()
this.setState({newlist: [this.props.list]})
}
// componentDidUpdate() {
// console.log(this.state.newlist);
// }
render() {
const MappedTarot = (this.state.newlist.map((list, i) => <ThreeCardMap key={i} name={list.name} meaningup={list.meaning_up} meaningdown={list.meaning_rev}/>);
return (
<div>
<h1>Three Card Reading</h1>
<div>{ MappedTarot }</div>
</div>
)
}
}
export default withProvider(Threecard);
Hi, I'm trying to create a page that takes data from a tarot card API (https://rws-cards-api.herokuapp.com/api/v1/cards/search?type=major). Unfortunately by the time the data comes in, my map function has already fired. I'm asking to see if there is a way to have the map function wait until the data hits before it fires. Thanks!
Edit: getList function in the Context:
getList = () => {
console.log('fired')
axios.get('https://vschool-cors.herokuapp.com?url=https://rws-cards-api.herokuapp.com/api/v1/cards/search?type=major').then(response =>{
this.setState({
list: response.data
})
}).catch(error => {
console.log(error);
})
}
this.props.getList() is an async function. You are setting the list right after that call which is not correct.
You need to set it in the getList promise then() block.
getList() is an async function and update data for the parent component. So, my solution is just watching the list from the parent component if they updated or not, through getDerivedStateFromProps
class Threecard extends Component {
constructor() {
super();
this.state = {
newlist: []
}
}
// Set props.list to this.state.newList and watch the change to update
static getDerivedStateFromProps(nextProps, prevState) {
return {
newlist: nextProps.list
}
}
componentDidMount(){
this.props.getList()
// Removed this.setState() from here.
}
render() {
const MappedTarot = (this.state.newlist.map((list, i) => <ThreeCardMap key={i} name={list.name} meaningup={list.meaning_up} meaningdown={list.meaning_rev}/>);
return (
<div>
<h1>Three Card Reading</h1>
<div>{ MappedTarot }</div>
</div>
)
}
}
export default withProvider(Threecard);

ReactJs - How to complete onClick before download - href

I have a simple React button component that when clicked should retrieve and download data on the client browser. The problem I am experiencing is that the download is triggered and the csv file downloaded before the data is passed into the href.
Here is my component:
import { Component } from 'react';
import { connect } from 'react-redux';
import { PropTypes } from 'prop-types';
import { ManageUsersSelectors } from 'selectors/Users';
import { BatchRoleActions } from 'actions/Users';
class UsersExportButton extends Component {
constructor() {
super();
this.state = {
users: ''
};
}
getUsers(){
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
this.setState({ users: users});
return this.state.users;
});
}
render() {
return (
<div className="roles-export-button">
<a className="button button-default" href={this.state.users} download={'roles.csv'} onClick={() => this.getUsers()} return true>Export Csv</a>
</div>
);
}
}
function mapStateToProps(state) {
const userIds = ManageUsersSelectors.batchUserIdsSelector(state);
return {
userIds: userIds
};
}
UsersExportButton.propTypes = {
text: PropTypes.string.isRequired,
data: PropTypes.array
};
export default connect(mapStateToProps)(UsersExportButton);
How can I get the getUsers()/onClick function to complete the data retrieval step before downloading?
When i debug my code I can see that the getUsers function returns data - however after the download is triggered
Make sure to bind this to your functions. In your constructor you can do:
constructor() {
super();
this.state = {
users: ''
};
this.getUsers = this.getUsers.bind(this);
}
or you can use the bind this function:
getUsers = () => {
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
this.setState({ users: users});
return this.state.users; // This should be removed, you can use this.state.users throughout this component.
});
}
Why not get the user data in the componentDidMount lifecycle method? It doesn't look like it needs to be called onClick.
{
// ...
componentDidMount() {
this.getUsers();
}
// ...
render() {
return (
<div className="roles-export-button">
<a className="button button-default" href={this.state.users} download={'roles.csv'}>Export Csv</a>
</div>
)
}
}
How about handling the default "link" behaviour manually to get more control? Also you should probably try to access state after setState has been executed via its callback.
e.g.
getUsers(cb){
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
// note the callback of setState which is invoked
// when this.state has been set
this.setState({ users: users }, cb);
});
}
const handleClick = () => {
this.getUsers(() => {
window.open(this.state.whatever)
})
}
<span onClick={handleClick}>Export Csv</span>

Component crashes when re-render | React, Redux

I have a strange problem where my react, redux app crashes when I re-render the component. The component that I'm talking about it this one, DoorsSettingsContainer. Which has it's own path:
<AuthRoute
exact
path="/settings/:itemId"
component={DoorsSettingsContainer}
/>
And when navigating to it the first time via a link:
<Link to={{ pathname: `/settings/${door._id}` }}>
<p className="sub-title-text-container">Inställningar</p>
</Link>
It works fine, but when I'm on the DoorsSettingsContainer and refreshes my page everything crashes. Here's my component (I removed my imports to reduce the length).
// NOTE: There's no data here so my app crashes :-(
const getDoorById = (reduxStore, door) => {
return reduxStore.fetchDoors.doors.find(item => item._id == door)
}
const getControllerById = (reduxStore, controllerId) => {
return reduxStore.fetchDoors.controllers.find(
item => item._id == controllerId
)
}
class DoorSettingsContainer extends Component {
componentDidMount() {
this.props.fetchDoors()
}
render() {
const door = this.props.doors || []
const controller = this.props.controllers || []
if (this.props.isLoading) return <CircularProgress />
return (
<div>
<DoorSettingsForm
onSubmit={this.props.updateSettings}
door={door}
id={this.props.match.params}
controller={controller}
/>
</div>
)
}
}
DoorSettingsContainer.propTypes = {
doors: PropTypes.object.isRequired,
controllers: PropTypes.object.isRequired,
fetchDoors: PropTypes.func.isRequired
}
const mapStateToProps = (state, ownProps) => {
const door = getDoorById(state, ownProps.match.params.itemId)
const doorId = ownProps.match.params.itemId
const controller = getControllerById(state, door.controller)
return {
doors: door,
controllers: controller,
isLoading: state.settings.isLoading
}
}
const mapDispatchToProps = dispatch => {
return {
fetchDoors: () => dispatch(fetchDoors()),
updateSettings: (id, door, controller) =>
dispatch(updateSettings(id, door, controller))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(
DoorSettingsContainer
)
And here's my error message:
I guess I should be mentioning that I am using async await for my action fetchDoors, so not regular promises. I've also read this post: Why is componentDidMount not being called when I re-render? but with no luck.
Thanks for reading and hopefully we can sort this out together.
You will first need to solve the crashing by doing a nullcheck on your Object
"reduxStore.fetchDoors.doors"
const getDoorById = (reduxStore, door) => {
return (!!reduxStore && !!reduxStore.fetchDoors) ? reduxStore.fetchDoors.doors.find(item => item._id == door) : null
}
the next step is to find out why your object "reduxStore.fetchDoors" is empty.
If i would be you, i would first go to your Reducer and troubleshoot if your store-state gets overwritten somewhere.

Categories

Resources