React Native & Flatlist, array items not available (.length === 0) - arrays

I'm experience an issue in react native where when the array i have in my state is behaving very strangely. It says i have content in the array, but the array.length === 0. Here is a screenshot of my chatrooms array to show you what i mean (this is at the beginning of my FlatList component (receiving props.chatrooms array)
console.log('props.chatrooms')
console.log(props.chatrooms)
console.log(props.chatrooms.length)
I'm grabbing all the private and group chatIds, then making a call to those chatrooms to get the details of the users. Here is how i'm pulling the information:
componentDidMount() {
this.setState({ isLoading: true })
this.props.navigation.addListener('focus', () => {
//? Refactor to pull userId from Redux Store
let friendsArray = []
let chatrooms = []
let privateChatIds = []
let groupChatIds = []
AsyncStorage.getItem('userId').then(userId => {
let currentUser = {}
firebase.firestore().collection('users').doc(userId).get().then(snapshot => {
currentUser = snapshot.data()
}).then(() => this.setState({ currentUser: currentUser }))
firebase.firestore().collection('users').doc(userId).collection('friends').get().then((snapshot) => {
if (!snapshot.empty) {
snapshot.forEach((doc) => {
friendsArray.push(doc.data())
privateChatIds.push(doc.data().chatroomId)
});
} else {
console.log('Nothing to Grab')
}
}).then(() => this.setState({ friends: friendsArray }))
// get each array and put into the chatroomsArray
firebase.firestore().collection('users').doc(userId).collection('groupChatrooms').get().then((snapshot) => {
if (!snapshot.empty) {
snapshot.forEach((doc) => {
groupChatIds.push(doc.data().key)
})
}
//! will want to limit this number to like 10-15 or something
}).then(() => {
for (let i = 0; i < privateChatIds.length; i++) {
firebase.firestore().collection('chatrooms').doc('private').collection(privateChatIds[i]).doc('info').get().then((snapshot) => {
chatrooms.push(snapshot.data())
})
}
for (let i = 0; i < groupChatIds.length; i++) {
firebase.firestore().collection('chatrooms').doc('group').collection(groupChatIds[i]).doc('info').get().then((snapshot) => {
chatrooms.push(snapshot.data())
})
}
}).then(() => {
console.log('chatrooms')
console.log(chatrooms)
this.setState({ chatrooms, isLoading: false })
})
})
})
}
is my code perfect? no, probably pretty terrible performance-wise, but i'm just trying to get it to work. I figure i'm resetting the chatrooms array or something, but why would it log as an array of items i want but then immediately after log as .length === 0??? Help!
Update: I'm seeing this content render from the flatlist initially and then it disappears

Related

Firestore: calling collections.get() inside promise()

useEffect(() => {
if (!stop) {
// get current user profile
db.collection('events').get(eventId).then((doc) => {
doc.forEach((doc) => {
if (doc.exists) {
let temp = doc.data()
let tempDivisions = []
temp["id"] = doc.ref.id
doc.ref.collection('divisions').get().then((docs) => {
docs.forEach(doc => {
let temp = doc.data()
temp["ref"] = doc.ref.path
tempDivisions.push(temp)
});
})
temp['divisions'] = tempDivisions
setEvent(temp)
setStop(true)
// setLoading(false);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
<Redirect to="/page-not-found" />
}
})
})
}
}, [stop, eventId]);
I am curious if this is the properly way to extract nested data from Cloud Firestore.
Data model:
Collection(Events) -> Doc(A) -> Collection(Divisions) -> Docs(B, C, D, ...)
Pretty much I'm looking to get metadata from Doc(A), then get all the sub-collections which contain Docs(B, C, D, ...)
Current Problem: I am able to get meta data for Doc(A) and its subcollections(Divisions), but the front-end on renders metadata of Doc(A). Front-End doesn't RE-RENDER the sub-collections even though. However, react devtools show that subcollections(Divisions) are available in the state.
EDIT 2:
const [entries, setEntries] = useState([])
useEffect(() => {
let active = true
let temp = []
if (active) {
divisions.forEach((division) => {
let teams = []
let tempDivision = division
db.collection(`${division.ref}/teams`).get().then((docs) => {
docs.forEach((doc, index) => {
teams.push(doc.data())
})
tempDivision['teams'] = teams
})
setEntries(oldArray => [...oldArray, temp])
})
}
return () => {
active = false;
};
}, [divisions]);
is there any reason why this is not detecting new array and trigger a new state and render? From what I can see here, it should be updating and re-render.
Your inner query doc.ref.collection('divisions').get() doesn't do anything to force the current component to re-render. Simply pushing elements into an array isn't going to tell the component that it needs to render what's in that array.
You're going to have to use a state hook to tell the component to render again with new data, similar to what you're already doing with setEvent() and setStop().

How to handle array state filter clashes

I am currently having an issue where multiple setStates that use the filtering of an array are interfering with each other. Basically if a user uploads two files, and they complete around the same time, one of the incomplete files may fail to be filtered from the array.
My best guess is that this is happening because they are separately filtering out the one that needs to be filtered, when the second one finishes and goes to filter itself out of the array, it still has the copy of the old incomplete array where the first file has not been filtered out yet. What would be a better way to approach this? Am I missing something obvious? I am thinking of using an object to hold the files instead, but then I would need to create a custom mapping function for the rendering part so that it can still be rendered as if were an array.
fileHandler = (index, event) =>{
let incompleteFiles = this.state.incompleteFiles
incompleteFiles[index].loading = true
incompleteFiles[index].file = event.target.files[0]
this.setState({ incompleteFiles: incompleteFiles },()=>{
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
let incompleteFiles = this.state.incompleteFiles
let completeFiles = this.state.completeFiles
api.uploadFile(fileData)
.then(res=>{
if(res.data.success){
this.setState(state=>{
let completeFile = {
name : res.data.file.name,
}
completeFiles.push(completeFile)
incompleteFiles = incompleteFiles.filter(inc=>inc.label !== res.data.file.name)
return{
completeFiles,
incompleteFiles
}
})
}
})
})
}
Updated with accepted answer with a minor tweak
fileHandler = (index, event) =>{
this.setState(({ incompleteFiles }) => ({
// Update the state in an immutable way.
incompleteFiles: [
...incompleteFiles.slice(0, index),
{
...incompleteFiles[index],
loading: true,
file: event.target.files[0],
},
...incompleteFiles.slice(index+1)
],
}), () => {
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
api.uploadFile(fileData)
.then(res => {
if(res.data.success){
this.setState(({ incompleteFiles, completeFiles }) => ({
completeFiles: [
...completeFiles, // Again, avoiding the .push since it mutates the array.
{ // The new file.
name: res.data.file.name,
}
],
incompleteFiles: incompleteFiles.filter(inc=>inc.label !== res.data.file.name),
})))
}
})
});
}
In class components in React, when setting the state which is derived from the current state, you should always pass a "state updater" function instead of just giving it an object of state to update.
// Bad
this.setState({ counter: this.state.counter + 1 });
// Good
this.setState((currentState) => ({ counter: currentState.counter + 1 }));
This ensures that you are getting the most up-to-date version of the state. The fact that this is needed is a side-effect of how React pools state updates under the hood (which makes it more performant).
I think if you were to re-write your code to make use of this pattern, it would be something like this:
fileHandler = (index, event) =>{
this.setState(({ incompleteFiles }) => ({
// Update the state in an immutable way.
incompleteFiles: {
[index]: {
...incompleteFiles[index],
loading: true,
file: event.target.files[0],
},
},
}), () => {
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
api.uploadFile(fileData)
.then(res => {
if(res.data.success){
this.setState(({ incompleteFiles, completeFiles }) => ({
completeFiles: [
...completeFiles, // Again, avoiding the .push since it mutates the array.
{ // The new file.
name: res.data.file.name,
}
],
incompleteFiles: incompleteFiles.filter(inc=>inc.label !== res.data.file.name),
})))
}
})
});
}
Another thing to keep in mind is to avoid mutating your state objects. Methods like Array.push will mutate the array in-place, which can cause issues and headaches.
I think change code to this can solve your problem and make code easy to read.
fileHandler = async (index, event) =>{
const incompleteFiles = [...this.state.incompleteFiles]
incompleteFiles[index].loading = true
incompleteFiles[index].file = event.target.files[0]
this.setState(
{
incompleteFiles
},
async (prev) => {
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
const res = await api.uploadFile(fileData)
/// set loading state to false
incompleteFiles[index].loading = false
if (!res.data.success) {
return { ...prev, incompleteFiles }
}
// add new file name into completeFiles and remove uploaded file name from incompleteFiles
return {
...prev,
completeFiles: [...prev.completeFiles, { name : res.data.file.name }],
incompleteFiles: incompleteFiles.filter(inc=>inc.label !== res.data.file.name)
}
})
)
}

I cant update my component state.. Do somebody understand how it fix?

I cant understand why my renderMovies() function dont wanna update my component state.data and i cant render component on my screen ?!
Everithing goes ok until renderMovies function.. I think this.setState(newState) in my fetchPostData function is working incorrect... Do somebody know how to fix it? I tried different ways but i cant solve this issue.
class Movies extends React.Component {
constructor(props) {
super(props)
this.state = { data: {}}
this.fetchPostData = this.fetchPostData.bind(this)
this.renderMovies = this.renderMovies.bind(this)
this.populatePageAfterFetch = this.populatePageAfterFetch.bind(this)
}
componentDidMount() {
this.fetchPostData()
}
fetchPostData() {
fetch(`http://localhost/reacttest/wp-json/wp/v2/movies?per_page=100`)
.then(response => response.json())
.then(myJSON => {
let objLength = Object.keys(myJSON).length
let newState = this.state;
for (let i = 0; i < objLength; i++) {
let objKey = Object.values(myJSON)[i].title.rendered;
// console.log(objKey)
let currentMovie = newState.data[objKey];
currentMovie = {};
currentMovie.name = Object.values(myJSON)[i].title.rendered;
currentMovie.description = Object.values(myJSON)[i].content.rendered;
currentMovie.featured_image = Object.values(myJSON)[i]['featured_image_url'];
currentMovie.genre = Object.values(myJSON)[i]['genre'];
}
this.setState(newState)
})
}
renderMovies() {
if(this.state.data) {
const moviesArray = Object.values(this.state.data)
console.log(moviesArray)
return Object.values(moviesArray).map((movie, index) => this.populatePageAfterFetch(movie, index))
}
}
populatePageAfterFetch(movie, index) {
if (this.state.data) {
return (
<div key={index} index={index}>
<h2>{movie.title}</h2>
<h3>{movie.genre}</h3>
<p>{movie.description}</p>
</div>
)
}
}
render() {
return (
<div>
<h1>Movies</h1>
<div>{this.renderMovies()}</div>
</div>
)
}
}
When i try to console.log(moviesArray) it show me:
Issue
You save current state into a variable named newState, never update it, and then save the same object reference back into state. React state never really updates.
let newState = this.state;
for (let i = 0; i < objLength; i++) {
...
}
this.setState(newState);
Additionally you mutate state
let currentMovie = newState.data[objKey];
currentMovie = {};
But this doesn't work either since initial state is an empty object so newState.data[objKey] is aways undefined. (so nothing is ever actually mutated)
Solution
It appears as though you intended to map the myJSON data/values into movie objects to update this.state.data. May I suggest this solution. The key is to always create new object references for any object you update.
fetchPostData() {
fetch(`http://localhost/reacttest/wp-json/wp/v2/movies?per_page=100`)
.then(response => response.json())
.then(myJSON => {
this.setState(prevState => ({
// array::reduce over the JSON values
data: Object.values(myJSON).reduce((movies, movie) => {
// compute movie key
const name = movie.title.rendered;
return {
...movies,
[name]: {
...movies[name], // copy any existing movie properties
// merge in new/updated properties
name,
description: movie.content.rendered,
featured_image: movie.featured_image_url,
genre: movie.genre,
},
}
}, { ...prevState.data }) // use previous state as initial value for reduce
}))
})
}

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.

ReactJS: Check if array contains value else append

I'm trying to check if a JSON response contains a value already inside an array and if it doesn't add it in. The problem I'm having is understanding how to approach this in reactjs. I'm checking before I append it but it doesn't want to work. I've tried passing in user object & user.id but these fail. The attempt below fails to compile but it should help understand what I'm trying to achieve.
Code:
componentWillMount() {
fetch('http://localhost:8090/v1/users')
.then(results => {
return results.json();
})
.then(data => {
data.map((user) => (
if(userList.hasOwnProperty(user.id)) {
userList.push({label: user.title, value: user.id})))
}
})
}
map return the resultant array, but you are not returning anything from it, you should instead use forEach Also you need to check if the userList array contains the id, for that you can use findIndex
What you need is
state = {
userList: [];
}
componentDidMount() {
fetch('http://localhost:8090/v1/users')
.then(results => {
return results.json();
})
.then(data => {
const newUserList = [...this.state.userList];
data.forEach((user) => { // use { here instead of
if(userList.findIndex(item => item.value === user.id) < 0) {
newData.push({label: user.title, value: user.id})
}
})
this.setState({userList: newUserList});
});
}
render() {
return (
{/* map over userList state and render it here */}
)
}
I'd recommend using reduce to turn the returned data into an array you'd like, then adding those values to your existing user list:
fetch('http://localhost:8090/v1/users')
.then(res => res.json())
.then(data => data.reduce((acc, user) => {
const idList = userList.map(user => user.id);
if (idList.indexOf(user.id) === -1) {
acc.push({label: user.title, value: user.id})
}
return acc;
},[]))
.then(newList => userList = [...userList, ...newList]);

Resources