How Can I Render Data From Firestore To React Native Component? - reactjs

I have been trying to render information from my firebase to a react native component. I started by console logging what I have done, the data is being fetched completely fine:
displayAllPlayers(){
dbh.collection('Players').get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
console.log(doc.data().First, doc.data().Last)
})
})}
I then tried to add this information to my component as follows:
displayAllPlayers(){
dbh.collection('Players').get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
<Player key={doc.data().First} fName={doc.data().First} lName={doc.data().Last} />
})
})
}
render() {
const myPlayers = this.displayAllPlayers()
}
return(
{myPlayers}
)

It's always suggested to create a different helpers file.
Create a firebase-helpers.js file which has an function to convert snapshot to array.
// FILENAME - firebase-helpers.js
export const snapshotToArray = (snapshot) => {
let returnArr = [];
snapshot.forEach((childSnapshot) => {
let item = childSnapshot.data();
returnArr.push(item);
});
return returnArr;
};
Now in your screen, import this file
import { snapshotToArray } from "../helpers/firebaseHelpers";
Then, convert snapshot of Players to array
const playersSnapshot = dbh.collection('Players').get();
const playersArray = snapshotToArray(playersSnapshot);
this.setState({ players : playersArray });
Now in state you have an array players. To display content of Players, you can use in your render function as -
<FlatList
data={this.state.players}
renderItem={({ item }, index) => this.playerDisplay(item, index)}
keyExtractor={(item, index) => index.toString()}
/>
You can then have a function to return details of players as -
playerDisplay = (item, index) => {
return(
<View>
<Text>
Player {index} - {item.First} {item.Last}
</Text>
</View>
);
}
I hope it works fine.

You should return the JSX inside the render function.
displayAllPlayers isn't returning anything.
In this snippet
querySnapshot.forEach(doc => {
<Player key={doc.data().First} fName={doc.data().First} lName={doc.data().Last} />
})
you're not returning anything inside the callback passed to forEach even if you do, it doesn't work because forEach doesn't return an array. You can use map here.
Maintain a state in the component and update it once you get the data. Use this state for rendering the array of JSX elements.

Related

how can conditional rendering reflect the state from a list of Boolean using hook?

The Goal:
My React Native App shows a list of <Button /> based on the value from a list of Object someData. Once a user press a <Button />, the App should shows the the text that is associated with this <Button />. I am trying to achieve this using conditional rendering.
The Action:
So first, I use useEffect to load a list of Boolean to showItems. showItems and someData will have the same index so I can easily indicate whether a particular text associated with <Button /> should be displayed on the App using the index.
The Error:
The conditional rendering does not reflect the latest state of showItems.
The Code:
Here is my code example
import {someData} from '../data/data.js';
const App = () => {
const [showItems, setShowItems] = useState([]);
useEffect(() => {
const arr = [];
someData.map(obj => {
arr.push(false);
});
setShowItems(arr);
}, []);
const handlePressed = index => {
showItems[index] = true;
setShowItems(showItems);
//The list is changed.
//but the conditional rendering does not show the latest state
console.log(showItems);
};
return (
<View>
{someData.map((obj, index) => {
return (
<>
<Button
title={obj.title}
onPress={() => {
handlePressed(index);
}}
/>
{showItems[index] && <Text>{obj.item}</Text>}
</>
);
})}
</View>
);
};
This is because react is not identifying that your array has changed. Basically react will assign a reference to the array when you define it. But although you are changing the values inside the array, this reference won't be changed. Because of that component won't be re rendered.
And furthermore, you have to pass the key prop to the mapped button to get the best out of react, without re-rendering the whole button list. I just used trimmed string of your obj.title as the key. If you have any sort of unique id, you can use that in there.
So you have to notify react, that the array has updated.
import { someData } from "../data/data.js";
const App = () => {
const [showItems, setShowItems] = useState([]);
useEffect(() => {
const arr = [];
someData.map((obj) => {
arr.push(false);
});
setShowItems(arr);
}, []);
const handlePressed = (index) => {
setShowItems((prevState) => {
prevState[index] = true;
return [...prevState];
});
};
return (
<View>
{someData.map((obj, index) => {
return (
<>
<Button
key={obj.title.trim()}
title={obj.title}
onPress={() => {
handlePressed(index);
}}
/>
{showItems[index] && <Text>{obj.item}</Text>}
</>
);
})}
</View>
);
};
showItems[index] = true;
setShowItems(showItems);
React is designed with the assumption that state is immutable. When you call setShowItems, react does a === between the old state and the new, and sees that they are the same array. Therefore, it concludes that nothing has changed, and it does not rerender.
Instead of mutating the existing array, you need to make a new array:
const handlePressed = index => {
setShowItems(prev => {
const newState = [...prev];
newState[index] = true;
return newState;
});
}

Avoiding a recursive re-render

Sending a react-redux action to an API to return posts when the user activates drag refresh or accesses the component for the first time.
This normally does not cause a problem as the FlatList would take directly from props and therefore not trigger a recursive loop. However, I need to format the data to fit the application and as a result, I pass the data through a series of methods before it hits the FlatList. This process is clashing with componentDidUpdate thus throwing the application into a loop of requests. Code below:
componentDidUpdate(prevProps) {
if (prevProps.posts !== this.props.posts){
this.storePosts() //the problem is that when storeposts is called it is updating the props which then is calling store posts which is updating the props and so on and so on....
}
}
storePosts() { //the problem with this is that it changes the state
this.props.fetchPosts() //this line is causing a continous request to rails
let itemData = this.props.posts
if (itemData !== this.state.currentlyDisplayed) {
this.setState({ currentlyDisplayed: this.props.posts, items: itemData }, () => {
this.setState({isLoading: false})
});
}
}
formatItems = () => {
const itemData = this.props.posts
const newItems = [itemData]
const numberOfFullRows = Math.floor(itemData.length / 3);
let numberOfElementsLastRow = itemData.length - (numberOfFullRows * 3);
while (numberOfElementsLastRow !== 3 && numberOfElementsLastRow !== 0) {
newItems.push({ key: `blank-${numberOfElementsLastRow}`, empty: true });
numberOfElementsLastRow++;
}
return this.setState({ newItems: newItems})
// console.log(newItems.length, numberOfElementsLastRow)
};
renderItem = ({ item, type }) => {
const { items } = this.state;
if (item.empty === true) {
return <View style={[styles.item, styles.itemInvisible]} />;
} else {
return (
<TouchableOpacity style={styles.item} onPressIn={() => this.setState({ itemSelected: item.id })} onPress={() => this.props.navigation.navigate('View Post', {post: item})} key={item.id}>
<Image source={{ uri: item.image }} style={{ flex: 1, width: '100%', height: undefined }} />
</TouchableOpacity>
);
}
};
onRefresh = () => {
this.setState({refreshing: true})
this.storePosts()
this.setState({refreshing: false, currentlyDisplayed: this.props.posts})
};
render() {
const { error: { vaultErrorMessage }} = this.props
const { posts } = this.props
<SafeAreaView>
<FlatList
data={this.state.currentlyDisplayed}
renderItem={this.renderItem}
numColumns={3}
keyExtractor={(item, index) => index.toString()}
refreshControl={<RefreshControl refreshing={this.state.refreshing} onRefresh={() => this.onRefresh()} />}
extraData={this.state.refresh}
/>
</SafeAreaView>
);
}
}
}
If anyone has any ideas to go about this better or solve the problem that would be great! I think I have been looking at the code for too long so I'm pretty dulled to thinking about how to solve....
I suggest splitting the logic for updating the posts and storing the posts into two separate methods to avoid the infinite state update.
For example:
componentDidUpdate(prevProps) {
if (shouldUpdate) { // some other condition
this.updatePosts();
}
if (prevProps.posts !== this.props.posts){
this.storePosts();
}
}
updatePosts() {
this.props.fetchPosts();
}
storePosts() {
let itemData = this.props.posts;
if (itemData !== this.state.currentlyDisplayed) {
this.setState({ currentlyDisplayed: this.props.posts, items: itemData }, () => {
this.setState({isLoading: false})
});
}
}
You should also look into a better way to check if the posts have actually changed since the array may have changed but the content may have stayed the same. (Note that [1, 2, 3] === [1, 2, 3] evaluates to false). fast-deep-equal is a good library for this, or you can come up with a custom solution.
If you have to use this approach, use static getDerivedStateFromProps instead of componentDidUpdate.
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.
This method exists for rare use cases where the state depends on changes in props over time. For example, it might be handy for implementing a <Transition> component that compares its previous and next children to decide which of them to animate in and out.
You can try it;
extraData={this.state.currentlyDisplayed}

Rendering views based on change of TextInput in react native

I'm trying to display list of events based on the search query dynamically.
The problem is that I'm always on the initial View and the renderSearch View is never called.
PastEvent is a function called from the primary redner of the class by scenemap
Please check comments in the code.
//to display the past events tab
PastEvents = () => {
const state = this.state;
let myTableData = [];
if (
state.PastEventList.length !== 0
) {
state.PastEventList.map((rowData) =>
myTableData.push([
this.renderRow(rowData)
])
);
}
function renderPast() {
console.log("im in render past") //shows
return (
<ScrollView horizontal={false}>
<Table style={styles.table}>
{myTableData.map((rowData, index) => (
<Row
key={index}
data={rowData}
style={styles.row}
textStyle={styles.rowText}
widthArr={state.widthArr}
/>
))}
</Table>
</ScrollView>
)
}
function renderSearch() {
console.log("im in render search") //never shows even after changing the text
let searchTable = [];
if (
this.state.seacrhPastList.length !== 0
) {
state.seacrhPastList.map((rowData) =>
searchTable.push([
this.renderRow(rowData)
])
);
}
return (
<ScrollView horizontal={false}>
<Table style={styles.table}>
{searchTable.map((rowData, index) => (
<Row
key={index}
data={rowData}
style={styles.row}
textStyle={styles.rowText}
widthArr={state.widthArr}
/>
))}
</Table>
</ScrollView>
)
}
return (
<View style={styles.container}>
<TextInput placeholder="Search for Events" onChangeText={text => this.onChangeSearch(text)}></TextInput>
{this.state.searching ? renderSearch() : renderPast()} //please check the onchangeSearch function
</View>
)
}
And the function of change is like that:
onChangeSearch = (text) => {
if (text.length > 0) {
let jsonData = {};
//get list of events
let url = "/api/FindEvents/" + text.toLowerCase()
ApiHelper.createApiRequest(url, jsonData, true).then(res => {
if (res.status == 200) {
this.state.seacrhPastList = res.data
this.state.searching= true //I was hoping this change will cause the render
}
})
.catch(err => {
console.log(err);
return err;
});
}
}
How can i change the events based on the query of the input ? Thank you
you need to use useState here
declare useState like this:
PastEvents = () => {
const [searching, setText] = useState(false);
change the searching state here:
if (res.status == 200) {
this.state.seacrhPastList = res.data
setText(true);
}
Hope this helps!
You're in a stateless component you shouldn't use "this" in any way, also you can't use state that way, you need to use react hooks
Import { useState } from 'react'
Then you can use state in a functional component
const [state, setState] = useState(initialvalue);

How to store one record into an existing array with AsyncStorage React Native

I have an existing array myItems and the list of items, fetched from API (array itemsFromApi) and rendered as a Flatlist. I want to store each item (not an array of items) into an existing array and save it local.
What i tried to do:
Fetch array of items from api and render it as an Flatlist.
Save each item to an existing array myItems (using useState)
Use JSON.stringify and JSON.parse to store an array myItems as a value via AsyncStorage
What i have:
Fetch items and storing it into array myItems works good.
In AsyncStorage is stored only one item, although i store an array
using AsyncStorage.
After re-render an array myItems is empty, AsyncStorage has only one item.
Here is peace of code:
// Array myItems, where i'd like to store the data
const [myItems, setMyItems] = useState([]);
// Array of data from api
const [itemsFromApi, setItemsFromApi] = useState([]);
// Fetch items from API and render it as a Flatlist, works good
const getItems = async () => {
const response = await api.get('/...');
setItemsFromApi(response.data)
};
<FlatList
data={itemsFromApi}
keyExtractor = { (item, index) => index.toString() }
renderItem={({ item })=>{
return (
<TouchableOpacity>
<Text>{item.id}</Text>
<Text>{item.title}</Text>
<Button title="Add" onPress={()=>{addItem(item))} />
</TouchableOpacity>
)
}}
/>
// Save fetched items to array myItems
const addItem = (item) => {
setMyItems([...myItems, item]);
storeData();
};
// trying to store array myItems using AsyncStorage
const storeData = async (myItems) => {
try {
await AsyncStorage.setItem('STORAGE_KEY', JSON.stringify(myItems));
Alert.alert('Saved', 'Successful');
} catch (error) {
Alert.alert('Error', 'There was an error.')
}
};
// trying to retrieve data
const retrieveData = async () => {
try {
const value = await AsyncStorage.getItem('STORAGE_KEY');
if (value !== null) {
console.log(JSON.parse(value))
}
} catch (error) {
console.log(error)
}
return null;
};
I'm definitely doing something wrong, but I don’t understand what.
Thanks in advance!
Use the code below.
<FlatList
data={itemsFromApi}
keyExtractor = { (item, index) => index.toString() }
renderItem={({ item })=>{
return (
<TouchableOpacity>
<Text>{item.id}</Text>
<Text>{item.title}</Text>
<Button title="Add" onPress={()=>{setMyItems([...myItems, item]))} />
</TouchableOpacity>
)
}}
/>
I found the solution.
It was:
const addItem = (item) => {
setMyItems([...myItems, item]);
storeData();
};
Modified:
const addItem = (item) => {
myItems.push(item); // <- Here is the trick
storeData();
setMyContacts([...myItems]);
};
When i set just setMyContacts(myItems), the list on the screen not be updated.
When i set setMyContacts([...myItems , item]) an item is duplicated in state and be shown at screen twice. Correct is setMyContacts([...myItems]).

Error "Cannot read property 'map' of undefined" while trying to read & display data from firestore via my react app

I am trying to display the data read from my firestore collection, but after getting the data from firestore, I tried using a map function to map & display the data accordingly but it ends up with an error:
TypeError: Cannot read property 'map' of undefined
Which may indicate that my events are in a wrong format for the map function or the events are empty.
Here is my code for more clarification:
const [events, setEvents] = React.useState();
// const events = [];
React.useEffect(() => {
db.collection("Events").get()
.then(querySnapshot => {
// const data = querySnapshot.docs.map(doc => doc.data());
let events = [];
querySnapshot.forEach(doc =>
events.push({ ...doc.data() })
)
console.log(events);
setEvents(events);
});
});
return (
<Grid item sm={4}>
{ events.map(event => (
<Card className={classes.card}>
<CardHeader
avatar={
loading ? (
<Skeleton variant="circle" width={40} height={40} />
) :
<Avatar
alt="Ted talk"
src={event.eventImgUrl}
/>
}
.... // code contiues
I expect the code to display data for each event accordingly
Since loading data from Firestore may take a while, the initial value of events is going to be null. From reading this answer, it seems you can pass in the initial value to React.useState(), in which case that line should be:
const [events, setEvents] = React.useState([]);
By initializing with an empty array ([]), the map function will just do nothing until the data is loaded.
Alternatively, you can only show the grid once the data is loaded, and show some sort of "loading..." message until that time. Something like:
if (items) {
return (
<Grid item sm={4}>
{ events.map(event => (
<Card className={classes.card}>
<CardHeader
avatar={
loading ? (
<Skeleton variant="circle" width={40} height={40} />
) :
<Avatar
alt="Ted talk"
src={event.eventImgUrl}
/>
}
.... // code continues
}
else {
return (
<div>Loading...</div>
)
}
I think someone could also try adding async await, worked for me just now, but I'm sure there are many other solutions like the above
React.useEffect( async () => {
const snapshot = await db.collection("Events").get()
if (snapshot.empty) {
console.log("No Docs found")
}
setEvents(
snapshot.docs.map((doc) => {
return {
...doc.data(),
documentID: doc.id,
}
})
);
});
});

Resources