React fetch request returning an empty array - javascript

I have a 3D globe that when you click on a country, returns an overlay of users.
I've previously had everything working just a file of randomised, mock user data in a local file. However I've now received the actual database (a AWS one) and I can't seem to get the fetch to return what I need. It should accept a signal from country click then return a list of users from this and show them in the pop up.
I've taken out the actual database for now (confidential) but this is working correctly on postman and a simplified fetch request I created. It doesn't seem to work within the wider context of this app.
At this stage, it doesn't break but it just returns an empty array in the console.
console.log output for this.state...
this is the state {overlay: true, users: Array(0), country: "Nigeria"}
import { onCountryClick } from "../Scene3D/AppSignals";
import OutsideClickHandler from "react-outside-click-handler";
import Users from "../Users";
import "./style.scss";
class Overlay extends Component {
constructor(props) {
super(props);
this.state = { overlay: false, users: [], country: [] };
this.openOverlay = this.openOverlay.bind(this);
this.closeOverlay = this.closeOverlay.bind(this);
this.fetchUsers = this.fetchUsers.bind(this);
}
openOverlay() {
this.setState({ overlay: true });
}
closeOverlay() {
this.setState({ overlay: false });
}
fetchUsers() {
fetch(
"**AWS***"
)
.then(response => response.json())
.then(data => this.setState({ users: data.users }));
}
onCountryClick = (country, users) => {
this.openOverlay();
this.fetchUsers();
this.setState({ country });
console.log("users", this.state.users);
};
componentDidMount() {
this.onCountrySignal = onCountryClick.add(this.onCountryClick);
}
componentWillUnmount() {
this.onCountrySignal.detach();
}
render() {
const country = this.state.country;
const users = this.state.users;
return (
<OutsideClickHandler
onOutsideClick={() => {
this.closeOverlay();
}}
>
<div className="users-container">
{this.state.overlay && (
<>
<h1>{country}</h1>
{users}
</>
)}
</div>
</OutsideClickHandler>
);
}
}
export default Overlay;```
[1]: https://i.stack.imgur.com/NWOlx.png

The problem is here
onCountryClick = (country, users) => {
this.openOverlay();
this.fetchUsers();
this.setState({ country });
console.log("users", this.state.users);
};
fetchUsers function is asynchronous and taken out from the original flow of execution, And the original execution stack continue to execute the console.log("users", this.state.users) which is empty at this time because fetchUsers function is not yet done executing. to prove it try to console log inside fetchUsers and you'll see who executed first.

Related

Use static fetch service

I have created an express mongoose api. I want to use that api from my React-application.
I want to create a service that would manage those api requests. But I am new in react-native and I can't use that service. I tried creating a static class but I cannot make it works. Here is an example :
// apiService.js
class ApiService {
static fetchUsers = () => {
return fetch('XXX/users')
.then((response) => {
return response.json()
.then((data) => {
return data;
})
})
.catch((error) => {
console.error(error);
});
}
}
export default ApiService;
And my screen
// UserScreen.js
import ApiService from '../services/apiService';
export default class UserScreen extends Component {
constructor(props) {
super(props);
this.state = {
users: [],
isLoading: true,
}
}
componentDidMount = () => {
let users = ApiService.fetchUsers();
this.setState({data: users});
this.setState({isLoading: false});
}
render() {
if (this.state.isLoading) {
return (
<View style={styles.container}>
<ActivityIndicator/>
</View>
)
} else {
return (
<View style={{ flex: 1, marginTop: 100 }}>
{
this.state.data.map((val, key) => {
return <TouchableOpacity
style={styles.homeButton}
key={key}
onPress={() => this.redirectHandler(val)}>
</TouchableOpacity>
})
}
</View>
)
}
}
}
I tried using async and wait but I can't find a way to retrieve data. The data are well retrieve in the apiService but I can't share them with the UserScreen.
How can I use a (static or not) class/function in react-native and get the data from the screen
Update
Here is what I tried with async
class ApiService {
static fetchUsers = async () => {
try {
let response = await fetch('XXXXX/users/');
let json = await response.json();
return json;
} catch (error) {
console.error(error);
}
}
}
export default ApiService;
And in my Userscreen
componentDidMount = async () => {
try {
let users = await ApiService.fetchUsers();
this.setState({isLoading: false});
this.setState({data: users});
} catch (error) {
console.log(error);
}
}
The problem lies in the setState that you are performing twice. If you look at the logic of the component, first we check for isLoading, if true we show some message/spinner otherwise we are showing a list of users/data.
Sequence of the Set State:
this.setState({isLoading: false});
this.setState({data: users});
Note that each setState triggers a re-render of the component, so in this case first we set isLoading to false (1st Re-Render) and then we set the data (2nd Re-Render)
The problem is, when 1st Re-Render is done, isLoading is set to false and the condition which we talked about above, now enters the "showing the user/data list" part. Another thing to note here is we have defined users: [] in state and when we are setting the users array (from the api call), we set that in a variable called data. The variable data is never defined in state so essentially it is "undefined".
Issue in your code:
this.state.data.map(...)
You cannot map over an undefined variable and this is where the problem lies. Doing so will throw an error saying "cannot read property map of undefined".
To fix this:
When setting the users list, instead of doing this.setState({ data: users }) just do this.setState({ users: users }) and change this.state.data.map( to users.map(
Also, unnecessary re-renders are costly and in case of React Native, they are costlier. Merge your setState(...) calls when possible. For example,
this.setState({
isLoading: false,
users: users
})

Consuming Paginated API in React Component

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

Re-render React component back to original state

I have this component that returns a bunch of li's on a axios get request, users input text and the search is updated..I want React to re-render the component when the searchInput is null, basically back to its original state.
class App extends Component {
constructor (props) {
super(props)
this.state = {
searchResults: [], // API Call returns an array of results
searchInput: '', // Search Term for API Call
searchImage: [] //base_url, a file_size and a file_path.
}
}
performSearch = () => { // Requesting data from API
axios.get(`${URL}api_key=${API_KEY}&language=en-US&query=${this.state.searchInput}${PARAMS}`)
.then((res) => {
console.log(res.data.results);
this.setState({ searchResults: res.data.results});
});
}
This function below is what triggers the rendering
handleInputChange = () => {
this.setState({
searchInput: this.search.value // User input
}, () => {
if (this.state.searchInput && this.state.searchInput.length >1 ) {
if (this.state.searchInput.length % 2 === 0) { // Request data on user input
this.performSearch();
} else if (this.state.searchInput && this.state.searchInput.length === 0 ) {
return ({ searchResults: null})
}
}
});
}
import React from 'react'
const Suggestions = (props) => {
const options = props.searchResults.map(r => (
<li
key={r.id} >
<img src={`https://image.tmdb.org/t/p/w185/${r.poster_path}`} alt={r.title} />
<a href='#t' className='rating'><i className='fas fa-star fa-fw' />{r.vote_average}</a>
</li>
))
return <ul className='search-results'>{options}</ul>
}
export default Suggestions
Issue at the moment is that is that if I search something, eg 'game of thrones' it renders the li's, however if I clear it back to an empty string, I still have left over li's...I dont't wanna see anything if the searchInput is null
Edit: performSearch is fires again while clearing searchInput and returns the last two characters which leaves me with left over li's
You haven't handled the conditions correct in the handleInputChange method. If the outer condition fails, the inner won't ever execute
handleInputChange = () => {
this.setState({
searchInput: this.search.value // User input
}, () => {
if (this.state.searchInput && this.state.searchInput.length >1 ) {
if (this.state.searchInput.length % 2 === 0) { // Request data on user input
this.performSearch();
}
} else {
this.now = Date.now();
this.setState({ searchResults: []})
}
});
}
Also the issue here could possible be the race condition with the API calls. It might so happen that when you clear the input although you setState to null or empty, the API then responds which sets the state again. The best way to handle is to accept only response that corresponds to the last request made
performSearch = () => { // Requesting data from API
let now = (this.now = Date.now());
axios.get(`${URL}api_key=${API_KEY}&language=en-US&query=${this.state.searchInput}${PARAMS}`)
.then((res) => {
console.log(res.data.results);
// Accepting response if this request was the last request made
if (now === this.now) {
this.setState({ searchResults: res.data.results});
}
});
}

React/Redux: how to call an API after getting data from the browser?

My React app is supposed to display nearby shops to each user depending on their location.
What I'm trying to do is to get the latitude/longitude by window.geolocation in the componentDidMount() in which I'll call a REST API with the latitude/longitude as params to retrieve nearby shops.
I'm just trying to figure out how to organize and where to put the logic.
The idea is that you should try to use React's state before reaching for redux. It's both simpler and keeps as much of the state local as possible.
The React state stores the status of the API call (not made yet, success or failure) and the corresponding information about success (results) or failure (errorMessage). The important point is that your render function must explicitly handle all 3 cases.
class NearbyShops extends React.Component {
constructor(props) {
super(props);
this.state = {
queryStatus: 'initial',
results: [],
errorMessage: '',
}
}
async componentDidMount() {
try {
const results = await fetch('you.api.url/here');
this.setState(prevState => ({...prevState, results, queryStatus: 'success'}))
} catch (e) {
this.setState(prevState => ({...prevState, queryStatus: 'failure', errorMessage: e}))
}
}
render() {
const {results, queryStatus, errorMessage} = this.state;
if (queryStatus === 'success') {
return (
<div>{/* render results here */}</div>
)
} else if (queryStatus === 'failure') {
return (
<div>{errorMessage}</div>
)
} else {
return (
<div>Loading nearby shops...</div>
)
}
}
}

React Native state isn't changing

I'm making a Ajax request to a Json file that return some movies.
state = { movies: [] };
componentWillMount()
{
this.getMovies();
}
/*
Make an ajax call and put the results in the movies array
*/
getMovies()
{
axios.get('https://pastebin.com/raw/FF6Vec6B')
.then(response => this.setState({ movies: response.data }));
}
/*
Render every movie as a button
*/
renderMovies()
{
const { navigate } = this.props.navigation;
return this.state.movies.map(movie =>
<ListItem key={ movie.title }
title={ movie.title }
icon={{ name: 'home' }}
onPress={() =>
navigate('Details', { title: movie.title, release: movie.releaseYear })
}
/>
);
}
render() {
return(
<List>
{ this.renderMovies() }
</List>
);
}
The error I get is the following: this.state.map is not a function. This is because movies is still empty.
When I console.log response.data it returns all the rows from the JSON file. So the problem is most likely in this line:
.then(response => this.setState({ movies: response.data }));
Does someone know what's wrong?
You put initial state in the wrong place. Do this instead:
constructor(props) {
super(props);
this.state = { movies: [] };
}
From document:
In general, you should initialize state in the constructor, and then
call setState when you want to change it.
Update you ajax request as following:
/*
Make an ajax call and put the results in the movies array
*/
getMovies()
{
let self = this;
axios.get('https://pastebin.com/raw/FF6Vec6B')
.then(response => self.setState({ movies: response.data }));
}
Also, you can bind your function inside constructor as:
constructor(props){
super(props);
this.getMovies = this.getMovies.bind(this);
}

Categories

Resources