React native data not rendered after setstate - reactjs

So i have been working with firebase as a backend in my react native application, i have tried to fetch data this way but i have nothing rendered, i have the activity indicator that went off, but i get that the data array is empty in the application screen, and when i do a console.log, i can see the data in the console, but nothing shows off in the application screen, please help me it's been days that i'm struggling.
export default class Leaderboard extends React.Component{
constructor(props){
super(props)
this.state = {
loading : true,
data : []
}
}
componentDidMount(){
firebase.firestore().collection('rankings').get()
.then(res => {
let rankArray = []
res.forEach(document => {
rankArray.push(document.data())
})
return rankArray;
}).then(res =>{
let data = []
res.forEach(item =>{
firebase.firestore().doc(item.idUser.path)
.get()
.then(doc =>{
let dataItem = {}
dataItem.id = doc.ref.path
dataItem.name = doc.data().fullname
dataItem.points = doc.data().points
dataItem.lc = 'Oran'
data.push(dataItem)
dataItem = {}
})
})
return data;
}).then(res =>this.setState({
loading : false,
data : res
}) ).catch(err => console.log(err))
}
render(){
if(this.state.loading){
return(
<View style = {styles.container}>
<ActivityIndicator size= 'large'></ActivityIndicator>
</View>
)
}else{
console.log(this.state.data)
return(
<View>
<Text>{this.state.data.length}</Text>
<FlatList
data={this.state.data}
renderItem={({item}) => <Text>{item.fullname}</Text>}
/>
</View>
)
}
}
}

The reason for this not working as expected is that you're trying to perform an asynchronous function call, per iteration of your res array inside of your forEach() callback:
// This is asynchronous
firebase.firestore().doc(item.idUser.path).get().then(doc =>{ ... })
Consider revising your code to use the Promise.all() method instead. This will ensure that each asynchronous for individual documents per-item in res array is completed, before setState() in the susequent .then() handler is invoked:
.then(res => {
let rankArray = []
res.forEach(document => {
rankArray.push(document.data())
})
return rankArray;
})
.then(res => {
// Use promise all to resolve each aync request, per item in the
// res array
return Promise.all(res.map(item => {
// Return promise from .get().then(..) for this item of res array.
return firebase.firestore()
.doc(item.idUser.path)
.get()
.then(doc => {
let dataItem = {}
dataItem.id = doc.ref.path
dataItem.name = doc.data().fullname
dataItem.points = doc.data().points
dataItem.lc = 'Oran'
// Return resolve dataItem to array that is relayed to next .then()
// handler (ie where you call this.setState())
return dataItem
})
}));
})
.then(res =>this.setState({
loading : false,
data : res
}))

Related

setState not returned from render when using Axios

I'm using axios to get data from an endpoint. I'm trying to store this data inside the state of my React component, but I keep getting this error:
Error: Results(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
I've struggled with many approaches: arrow functions etc., but without luck.
export default class Map extends Component {
constructor() {
super();
this.state = {
fillColor: {},
selectedCounty: "",
dbResponse: null,
};
}
getCounty(e) {
axios.get("/getWeatherData?county=" + e.target.id)
.then((response) => {
this.setState(prevState => {
let fillColor = {...prevState.fillColor};
fillColor[prevState.selectedCounty] = '#81AC8B';
fillColor[e.target.id] = '#425957';
const selectedCounty = e.target.id;
const dbResponse = response.data;
return { dbResponse, selectedCounty, fillColor };
})
}).catch((error) => {
console.log('Could not connect to the backend');
console.log(error)
});
}
render() {
return (
<div id="map">
<svg>big svg file</svg>
{this.state.selectedCounty ? <Results/> : null}
</div>
)
}
I need to set the state using prevState in order to update the fillColor dictionary.
Should this be expected? Is there a workaround?

How to properly paginate data in React with Firestore?

As I am starting my experience with Firebase I am a little bit struggling with the pagination of posts on my blog website :(.
I think I kind of understood the docs from Google and I know how to move the pagination to the next page. However, I have absolutely no clue how to paginate back to the previous page.
Basically, I wanted to have a simple pagination component which will look something like that: < 1 2 3 [...] > (where you can paginate next and back using the arrows).
It is fine to paginate to the next page but, when it comes to paginating back I cannot find any proper tutorial to do it in pure React.
I have tried to use various methods from startAt, endAt, endBefore etc. But the result was or an error or it was moving me back to the first page (even when I was on the third or fourth)
I even tried to find the first object in an array and use it as endBefore but it resulted again in paginating back to the first page.
That's how my code looks right now (yes I know that pageNext() and pagePrev() are the same)
import React, { Component } from 'react'
import { withFirebase } from './Firebase'
import Post from './Post'
import '../scss/Post.scss'
class Posts extends Component {
constructor(props) {
super(props);
this.state = {
loading:false,
posts:[],
post_id:[],
lastVisible:null,
limit:2
}
this.handlePageNext = this.handlePageNext.bind(this);
}
componentDidMount() {
let newPosts=[];
let postsId=[];
this.setState({ loading: true });
this.props.firebase.posts()
.orderBy('date', 'desc')
.limit(2)
.get().then(querySnapshot => {
let lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
this.setState({ lastVisible: lastVisible});
querySnapshot.forEach(doc => {
newPosts = newPosts.concat(doc.data());
postsId = postsId.concat(doc.id);
this.setState({
posts:newPosts,
post_id:postsId,
loading:false
});
})
})
}
handlePageNext() {
let newPosts=[];
let postsId=[];
this.setState({ loading: true });
this.props.firebase.posts()
.orderBy('date', 'desc')
.startAt(this.state.lastVisible)
.limit(this.state.limit)
.get().then(querySnapshot => {
let lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
this.setState({ lastVisible:lastVisible });
querySnapshot.forEach(doc => {
newPosts = newPosts.concat(doc.data());
postsId = postsId.concat(doc.id);
this.setState({
posts:newPosts,
post_id:postsId,
loading:false
});
})
})
}
handlePagePrev() {
let newPosts=[];
let postsId=[];
this.setState({ loading: true });
this.props.firebase.posts()
.orderBy('date', 'desc')
.startAt(this.state.lastVisible)
.limit(this.state.limit)
.get().then(querySnapshot => {
let lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
this.setState({ lastVisible:lastVisible});
querySnapshot.forEach(doc => {
newPosts = newPosts.concat(doc.data());
postsId = postsId.concat(doc.id);
this.setState({
posts:newPosts,
post_id:postsId,
loading:false
});
})
})
}
render() {
return (
<div className='posts'>
<div className='row'>
{this.state.posts.map((post, i) => (
<Post
key={i}
title={post.title}
author={post.author}
desc={post.desc}
text={post.text}
id={this.state.post_id[i]}
date={post.date}
imgURL={post.imgURL}/>
))}
{this.state.loading && <p>Loading...</p>}
<button className='btn' onClick={() => this.handlePagePrev()}>←</button>
<button className='btn' onClick={() => this.handlePageNext()}>></button>
</div>
</div>
)
}
}
export default withFirebase(Posts);
I wanted to have a simple pagination using buttons (left and right arrows) but I am struggling with it for already 3rd hour and cannot find the proper solution to this.
You have to keep the "lastVisible" and pass it to startAfter(). 2 functions I wrote below:
export const getMostRecentPostsFirstPage = (limit, specificUserId) => {
if (!Number.isInteger(limit) || limit < 1) {
throw new Error('limit must be a positive integer');
}
const collection = Firestore.collection('posts');
let query = null;
if (specificUserId) {
query = collection
.where('userId', '==', `${specificUserId}`)
.orderBy('postedTimestamp', 'desc')
.limit(limit);
} else {
query = collection.orderBy('postedTimestamp', 'desc').limit(limit);
}
return new Promise((resolve, reject) => {
const posts = [];
query
.get()
.then(snapshot => {
const lastVisible = snapshot.docs[snapshot.docs.length - 1];
snapshot.forEach(post => {
posts.push(post.data());
});
const hasMore = posts.length == limit;
resolve({ posts: posts, lastVisible: lastVisible, hasMore: hasMore });
})
.catch(error => reject(error));
});
};
export const getMostRecentPostsNextPage = (lastVisible, limit, specificUserId) => {
if (!lastVisible) {
throw new Error('Need to provide lastVisible argument');
}
if (!Number.isInteger(limit) || limit < 1) {
throw new Error('limit must be a positive integer');
}
const collection = Firestore.collection('posts');
let query = null;
if (specificUserId) {
query = collection
.where('userId', '==', `${specificUserId}`)
.orderBy('postedTimestamp', 'desc')
.startAfter(lastVisible)
.limit(limit);
} else {
query = collection
.orderBy('postedTimestamp', 'desc')
.startAfter(lastVisible)
.limit(limit);
}
return new Promise((resolve, reject) => {
const posts = [];
query
.get()
.then(snapshot => {
const lastVisible = snapshot.docs[snapshot.docs.length - 1];
snapshot.forEach(post => {
posts.push(post.data());
});
const hasMore = posts.length == limit;
resolve({ posts: posts, lastVisible: lastVisible, hasMore: hasMore });
})
.catch(error => reject(error));
});
};
It uses redux-saga, but you get the idea.
on first query, do not call "startAfter()", but do on the subsequent queries, and you must save "lastVisible" between each call.
Here is standard pagination by using Firebase in reactjs.

Unable to pass params successfully to another .js file/screen

I'm trying to pass params from one screen to another screen using react-navigation, the problem I'm encountering is that when I console.log the param itself, the console returns 'undefined'. I can't seem to pinpoint what I'm doing wrong exactly. Any help or guidance would be much appreciated.
I tried the following, with no success:
-this.props.navigation.getParam('biometryStatus')
-this.props.navigation.state.params('biometryStatus')
This is my AuthenticationEnroll screen where my param is being initialised as the state of the component:
export default class AuthenticationEnroll extends Component {
constructor() {
super()
this.state = {
biometryType: null
};
}
async _clickHandler() {
if (TouchID.isSupported()){
console.log('TouchID is supported');
return TouchID.authenticate()
.then(success => {
AlertIOS.alert('Authenticated Successfuly');
this.setState({biometryType: true })
this.props.navigation.navigate('OnboardingLast', {
pin: this.props.pin,
biometryStatus: this.state.biometryType,
});
})
.catch(error => {
console.log(error)
AlertIOS.alert(error.message);
});
} else {
this.setState({biometryType: false });
console.log('TouchID is not supported');
// AlertIOS.alert('TouchID is not supported in this device');
}
}
_navigateOnboardingLast() {
this.props.navigation.navigate('OnboardingLast', {pin: this.props.pin})
}
render () {
return (
<View style={{flex: 1}}>
<Slide
icon='fingerprint'
headline='Secure authentication'
subhead='To make sure you are the one using this app we use authentication using your fingerprints.'
buttonIcon='arrow-right'
buttonText='ENROLL'
buttonAction={() => this._clickHandler()}
linkText={'Skip for now.'}
linkAction={() => this._navigateOnboardingLast()}
slideMaxCount={4}
slideCount={2}
subWidth={{width: 220}}
/>
</View>
)
}
}
And this is my OnboardingLast Screen where my param is being passed down and printed through console.log:
class OnboardingLast extends Component {
async _createTokenAndGo () {
let apiClient = await this._createToken(this.props.pin)
this.props.setClient(apiClient)
AsyncStorage.setItem('openInApp', 'true')
const { navigation } = this.props;
const biometryStatus = navigation.getParam('biometryStatus', this.props.biometryStatus);
console.log(biometryStatus);
resetRouteTo(this.props.navigation, 'Home')
}
/**
* Gets a new token from the server and saves it locally
*/
async _createToken (pin) {
const tempApi = new ApiClient()
let token = await tempApi.createToken(pin)
console.log('saving token: ' + token)
AsyncStorage.setItem('apiToken', token)
return new ApiClient(token, this.props.navigation)
}
render () {
return (
<View style={{flex: 1}}>
<Slide
icon='checkbox-marked-circle-outline'
headline={'You\'re all set up!'}
subhead='Feel free to start using MyUros.'
buttonIcon='arrow-right'
buttonText='BEGIN'
buttonAction={() => this._createTokenAndGo()}
slideMaxCount={4}
slideCount={3}
/>
</View>
)
}
}
Expected Result is that console.log(biometryStatus); returns 'true' or 'false', however it returns 'undefined'.
Since setState is asynchron, you send null (declared in your constructor) to your next page. By doing so, you will send true:
this.setState({ biometryType: true })
this.props.navigation.navigate('OnboardingLast', {
pin: this.props.pin,
biometryStatus: true,
});
You could also do this, since setState can take a callback as param:
this.setState({ biometryType: true }, () => {
this.props.navigation.navigate('OnboardingLast', {
pin: this.props.pin,
biometryStatus: true,
});
})
In your second page this.props.biometryStatus is undefined.
The second argument of getParam is the default value. You should change it like that
const biometryStatus = navigation.getParam('biometryStatus', false);

React setState to array, then console.log(array) return filled array of objects, but console.log(array.length) is 0

so I fetch some data from two urls in order and save the responded data into an array, and then setState to this array. When I console.log(dataArray), I can see the contents, but the console.log(dataArray.length) is 0, no matter I check immediately after setState statement or in render method. what should I do to set state to array before it get rendered again? Thanks!!!
class App extends Component {
state={
dataArray:[]
};
componentDidMount(){
this.getTrainInfo();
}
getTrainInfo=()=>{
let arr=[];
let data={};
fetch('https://api-v3.mbta.com/predictions?filter[stop]=South+Station,North+Station&filter[direction_id]=0&include=vehicle&sort=departure_time')
.then(response=>response.json())
.then(res=> {
res.data.map(dat => {
data.departureTime = dat.attributes.departure_time;
data.boardingStaus = dat.attributes.status;
data.trainID = dat.relationships.route.data.id;
return data;
})
.forEach(data=>{
fetch('https://api-v3.mbta.com/routes/' + data.trainID)
.then(response2 => response2.json())
.then(res2 => {
data.destination = res2.data.attributes.direction_destinations[0];
arr.push(data);
//setState here work, but set too many times
//this.setState({dataArray:arr})
})
});
})
.then( ()=>{
this.setState({dataArray:arr});
console.log(this.state.dataArray); //log an array full of objects.
console.log(this.state.dataArray.length); //but here length is 0.
})
};
render() {
let data=this.state.dataArray;
let departureTime;
let boardingStatus;
let TrackNo;
let destination;
console.log(data); //log the array full of objects.
console.log(data.length); //but here the length is 0.
if(data.length){
departureTime=new Date(data[0].departureTime).toLocaleString('en-US',{hour:'numeric',minute:'numeric', hour12:true});
boardingStatus= data[0].boardingStaus;
TrackNo=(data[0].trackNo)?data[0].trackNo:0;
destination=data[0].destination;
}
return (
<div className="App">
<h1>Train info</h1>
<h2>{departureTime}
{boardingStatus}
{TrackNo}
{destination}
</h2>
</div>
);
}
}
This part of your code run before your requests is done inside forEach function, so arr can be empty here
then( ()=>{
this.setState({dataArray:arr});
console.log(this.state.dataArray); //log an array full of objects.
console.log(this.state.dataArray.length); //but here length is 0.
})
You can use prmoise.all for this purpose
getTrainInfo=()=>{
let arr=[];
let data={};
fetch('https://api-v3.mbta.com/predictions?filter[stop]=South+Station,North+Station&filter[direction_id]=0&include=vehicle&sort=departure_time')
.then(response=>response.json())
.then(res=> {
let changes = res.data.map(dat => {
data.departureTime = dat.attributes.departure_time;
data.boardingStaus = dat.attributes.status;
data.trainID = dat.relationships.route.data.id;
return data;
});
let requests = changes.forEach(data=>{
return fetch('https://api-v3.mbta.com/routes/' + data.trainID)
.then(response2 => response2.json())
.then(res2 => {
data.destination = res2.data.attributes.direction_destinations[0];
arr.push(data);
return res2
})
});
Promise.all(requests).then((values) => {
this.setState({dataArray:arr});
});
})
};
Thanks a lot to #aseferov !!!
so turns out I have to use Promise.all() to make sure setState will update all array after all fetches finished.
getTrainInfo=()=>{
let arr=[];
let data={};
fetch('https://api-v3.mbta.com/predictions?filter[stop]=South+Station&filter[direction_id]=0&include=vehicle&sort=departure_time')
.then(response=>response.json())
.then(res=> {
let changes= res.data.map(dat => {
data.departureTime = dat.attributes.departure_time;
data.trackNo=res.data[0].relationships.vehicle.data.id? res.data[0].relationships.vehicle.data.id:0;
data.boardingStaus = dat.attributes.status;
data.trainID = dat.relationships.route.data.id;
return 'https://api-v3.mbta.com/routes/' + data.trainID
});
//let changes=['https://api-v3.mbta.com/routes/CR-Franklin','https://api-v3.mbta.com/routes/Red','https://api-v3.mbta.com/routes/Orange','https://api-v3.mbta.com/routes/Mattapan']
//console.log(changes); //an array of urls
let requests=changes.map(change=>{return fetch(change).then(response=>response.json()).then(res=> res.data)}); //.then(response=>response.json()).then(res=> res.data)
//console.log(requests);
Promise.all(requests)
.then(responses=>responses.forEach(response=>{
console.log(response); //returns Objects: res.data[0], res.data[1],...
let destination=response.attributes.direction_destinations[0];
data.destination=destination;
arr.push(data);
}))
.then(()=>{console.log(arr); this.setState({dataArray:arr}); })
})
};

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

Resources