Integrating Firebase Realtime Database with React Native App - javascript

I am for the first time learning about state, and followed a simple tutorial to create a react native app. The tutorial did not cover using firebase, so this is what I've pieced together. It "works", but does not pull the database data on the first render. I know it's because of the delay in time it takes to grab the data vs my app rendering. But I don't know how I should move logic around to fix it. I feel like I should be using the .then somehow? Or am I doing all of this completely wrong...
import {db} from '../../src/config.js';
let initialMessages = [];
db.ref().once('value', (snapshot) =>{
snapshot.forEach((child)=>{
child.forEach(function(childChildSnapshot) {
initialMessages.push({
id: childChildSnapshot.key,
title: childChildSnapshot.val().title,
})
})
})
})
.then()
.catch((error) => {console.error('Error:', error)});
function MessagesScreen(props) {
const [messages, setMessages] = useState(initialMessages);
return (
<Screens>
<View style={styles.wholeThing}>
<FlatList
data={messages}
keyExtractor={(messages) => messages.id.toString()}
renderItem={({ item }) => (
<Card
title={item.title}
onPress={() => console.log("hi")}
/>
)}
ItemSeparatorComponent={ListItemSeparator}
contentContainerStyle={styles.messagesList}
refreshing={refreshing}
onRefresh={}
/>
</View>
</Screens>
);
}
export default MessagesScreen;

By the time you pass initialMessages to the state hook (as its initial value), the initialMessages.push(...) hasn't been called yet.
Instead, you need to call setMessages when the data has loaded:
db.ref().once('value', (snapshot) =>{
snapshot.forEach((child)=>{
child.forEach(function(childChildSnapshot) {
initialMessages.push({
id: childChildSnapshot.key,
title: childChildSnapshot.val().title,
})
})
setMessages(initialMessages);
})
})
Calling setMessages will then rerender the (affected) UI, and that will show the messages.
This of course means that you'll need to pull the useState hook out of MessagesScreen, so that it's also visible in the location where you now call setMessages.

Related

Adding objects to an array of objects with useState hook in React then rendering

I'm working on web scraping a news website to show top headlines and URLs. In my backend, I put each title and URL in an object within an array, which works fine. When I fetch this information in React, it is not working as expected.
I used the useState hook to initialize an empty array, where I would put each object containing the title and URL. Then, I map through that array and render each title and URL to the page.
When I refresh the webpage, it takes several seconds for each title and URL to pop up, and additionally, they are all the same title and URL.
It seems that my array is not being updated properly by putting in the same article information each time I set the state. However, I do not understand why it is taking so long for all the information to show up on the webpage, and do not know how to make it a faster process. I want all the information to be on the page after the user hits refresh, not appear after several seconds at a time. Could anyone help me see where I'm going wrong?
Here is my code:
import {useState} from 'react';
const News = () => {
const [articles, setArticles] = useState([])
fetch('http://localhost:8000/news')
.then(response => {return response.json()})
.then(data => {
data.forEach(article => {
setArticles([...articles, {
title: article.article_title,
url: article.article_url}])
})
})
.catch(err => console.log(err))
return (
<div>
{articles.map(article => {
return (
<div className="indv-article">
<h1 className="article-title" key={article.title}>{article.title}</h1>
<p className='article-url' key={article.url}>{article.url}</p>
</div>);
})}
</div>
)
}
export default News
Couple of things that may solve your issues.
First: In React, all side-effects (such as data fetching, for example) should be handled inside a useEffect hook.
Second: As stated in #Khorne07's answer, the key attribute should be on the root DOM node that is being returned from the map.
Third: I don't really know the purpose of looping through your data to set the state. If the reason you are doing this is because the response contains other information that you are not interested to display and you just want the title and url for each article, I suggest you to create an adapter function that will receive this data as a parameter and return just the information that you are interested in.
Additional: You can use a loading state to show a loading indicator while the data is being fetched and improve user experience.
Putting it all together:
const News = () => {
const [articles, setArticles] = useState([])
const [loading, setLoading] = useState(false)
const adaptArticles = (data) => {
return data.map(({ article_title, article_url }) => ({
title: article_title,
url: article_url
}))
}
useEffect(() => {
setLoading(true)
fetch('http://localhost:8000/news')
.then(response => {return response.json()})
.then(data => setArticles((prevArticles) => prevArticles.concat(adaptArticles(data))))
.catch(err => console.log(err))
.finally(() => {
setLoading(false)
})
}, []) //Insert the corresponding dependencies (if any) in the dependencies array so the useEffect hook gets executed when any of these dependencies change.
if(loading) //Return some loading indicator, like a Spinner for example.
return (
<div>
{articles.map(article => {
return (
<div className="indv-article" key={article.title}>
<h1 className="article-title">{article.title}</h1>
<p className='article-url'>{article.url}</p>
</div>);
})}
</div>
)
}
Edited:
You have some errors on your current code:
First of all, and, as mentioned by the accepted answer, all side effects like fetch calls should be inside a useEffect hook.
The second error is related to the way you are updating your state array. When your new state depends on the previous state value, you should use the callback function inside your setState function, in order to have your data correctly synchronized with the previous value. And in this particular example you are also calling a setState function inside a loop, which is a bad idea and can potentially drive your app into unexpected behavior. The best approach is described in the code snippet bellow.
fetch('http://localhost:8000/news')
.then(response => {return response.json()})
.then(data => {
const articlesArray = []
data.forEach(article => {
articlesArray.push({
title: article.article_title,
url: article.article_url
})
setArticles(currentArticles => [...currentArticles, ...articlesArray])
})
})
.catch(err => console.log(err))
And on the map function, the key attribute should be on the root node you are returning:
{articles.map(article => {
return (
<div className="indv-article" key={article.title}>{article.title}>
<h1 className="article-title"</h1>
<p className='article-url'</p>
</div>);
})}

Saving api response to State using useState and Axios (React JS)

I'm having an issue when trying to save to State an axios API call. I've tried
useState set method not reflecting change immediately 's answer and many other and I can't get the state saved. This is not a duplicate, because I've tried what the accepted answer is and the one below and it still doesn't work.
Here's the (rather simple) component. Any help will be appreciated
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // returns '[]'
});
}, []); // If I set 'widgets' here, my endpoint gets spammed
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Welcome to stackoverflow, first thing first the setting call is incorrect you must use spread operator to combine to array into one so change it to setWidgets([...widgets, ...data]); would be correct (I assume both widgets and data are Array)
second, react state won't change synchronously
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // <--- this will output the old state since the setWidgets above won't do it's work till the next re-render
so in order to listen to the state change you must use useEffect hook
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
this will console log anytime widget changes
the complete code will look like this
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
setWidgets([...widgets, ...data])
});
}, []);
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Try:
setWidgets(data);
istead of
setWidgets(widgets, data);
Your widgets.map() probably fails because there isn't much to map over when the component is being rendered.
You should update it with a conditional like so, just for clarity:
widgets.length>0 ? widgets.map(...) : <div>No results</div>
And your call to setWidgets() should only take one argument, the data:
setWidgets(data)
or if you want to merge the arrays use a spread operator (but then you need to add widgets as the dependency to the useEffect dependency array.
setWidgets(...widgets, ...data)
You might also have to supply the setWidgets hook function to the useEffect dependency array.
Let me know if this helps..

Implement RSS feed in React native

So, I've been learning react native lately and I am trying to create an RSS reader.I have managed to Download the RSS data but I am stuck in implementing them into JSX ( Screen). I've been trying using setState method but it didn't work.
I can get the RSS data logged into Console but I can't display them in user through JSX
PS: Comments are just tests that I made
class HomeScreen extends React.Component {
state = {
feed: {},
items: {}
};
RSS() {
return fetch("http://di.ionio.gr/feed/")
.then(response => response.text())
.then(responseData => rssParser.parse(responseData))
.then(rss => {
this.setState(() => ({
//...prevState,
feed: rss
//titles: rss.items,
}));
});
}
render() {
{this.RSS();}
return (
<View style={styles.top}>
<HeaderScreen {...this.props} />
<Text>{this.state.feed.title}</Text>
</View>
);
}
}
I've been using react-native-rss-parser.
I also tried without setState but that also didn't work.
Thanks in advance
With your current setup, you're creating an endless loop. This is because you have a side effect (i.e. network request) in your render function.
When your request returns and you call setState your component is rendered again, which in turn calls the network again with this.RSS(), etc. etc. etc.
To fix this, move the this.RSS() to either your constructor function or better yet to the componentDidMount function.
i.e.
componentDidMount() {
this.RSS();
}
render() {
return (
<View style={styles.top}>
<Text>{this.state.feed.title}</Text>
</View>
);
}

How do I pass a value from a promise to a component prop in react native?

Edit: I don't understand the reason for downvotes, this was a good question and no other questions on this site solved my issue. I simply preloaded the data to solve my issue but that still doesn't solve the problem without using functional components.
I'm trying to pass users last message into the ListItem subtitle prop but I can't seem to find a way to return the value from the promise/then call. It's returning a promise instead of the value which gives me a "failed prop type". I thought about using a state but then I don't think I could call the function inside the ListItem component anymore.
getMsg = id => {
const m = fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(msg => {
return msg;
});
return m;
};
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={this.getMsg(item.id)} // failed prop type
bottomDivider
chevron
/>
);
You could only do it that way if ListItem expected to see a promise for its subtitle property, which I'm guessing it doesn't. ;-) (Guessing because I haven't played with React Native yet. React, but not React Native.)
Instead, the component will need to have two states:
The subtitle isn't loaded yet
The subtitle is loaded
...and render each of those states. If you don't want the component to have state, then you need to handle the async query in the parent component and only render this component when you have the information it needs.
If the 'last message' is something specific to only the ListItem component and not something you have on hand already, you might want to let the list item make the network request on its own. I would move the function inside ListItem. You'll need to set up some state to hold this value and possibly do some conditional rendering. Then you'll need to call this function when the component is mounted. I'm assuming you're using functional components, so useEffect() should help you out here:
//put this is a library of custom hooks you may want to use
// this in other places
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
const ListItem = ({
title,
bottomDivider,
chevron,
onPress,
id, //hae to pass id to ListItem
}) => {
const [lastMessage, setLastMessage] = useState(null);
const isMounted = useIsMounted();
React.useEffect(() => {
async function get() {
const m = await fireStoreDB.getUserLastMessage(
fireStoreDB.getUID,
id
);
//before setting state check if component is still mounted
if (isMounted.current) {
setLastMessage(m);
}
}
get();
}, [id, isMounted]);
return lastMessage ? <Text>DO SOMETHING</Text> : null;
};
I fixed the issue by using that promise method inside another promise method that I had on componentDidMount and added user's last message as an extra field for all users. That way I have all users info in one state to populate the ListItem.
componentDidMount() {
fireStoreDB
.getAllUsersExceptCurrent()
.then(users =>
Promise.all(
users.map(({ id, username }) =>
fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(message => ({ id, username, message }))
)
)
)
.then(usersInfo => {
this.setState({ usersInfo });
});
}
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate('Chat', {
userTo: item.id,
UserToUsername: item.username
});
}}
title={item.username}
subtitle={item.message}
bottomDivider
chevron
/>
);

Access query inside return method

I have a backend Drupal site and react-native app as my frontend. I am doing a graphQL query from the app and was able to display the content/s in console.log. However, my goal is to use a call that query inside render return method and display it in the app but no luck. Notice, I have another REST API call testName and is displaying in the app already. My main concern is how to display the graphQL query in the app.
Below is my actual implementation but removed some lines.
...
import gql from 'graphql-tag';
import ApolloClient from 'apollo-boost';
const client = new ApolloClient({
uri: 'http://192.168.254.105:8080/graphql'
});
client.query({
query: gql`
query {
paragraphQuery {
count
entities {
entityId
...on ParagraphTradingPlatform {
fieldName
fieldAddress
}
}
}
}
`,
})
.then(data => {
console.log('dataQuery', data.data.paragraphQuery.entities) // Successfully display query contents in web console log
})
.catch(error => console.error(error));
const testRow = ({
testName = '', dataQuery // dataQuery im trying to display in the app
}) => (
<View>
<View>
<Text>{testName}</Text> // This is another REST api call.
</View>
<View>
<Text>{dataQuery}</Text>
</View>
</View>
)
testRow.propTypes = {
testName: PropTypes.string
}
class _TestSubscription extends Component {
...
render () {
return (
<View>
<FlatList
data={this.props.testList}
...
renderItem={
({ item }) => (
<testRow
testName={item.field_testNameX[0].value}
dataQuery={this.props.data.data.paragraphQuery.entities.map((dataQuery) => <key={dataQuery.entityId}>{dataQuery})} // Here I want to call the query contents but not sure how to do it
/>
)}
/>
</View>
)
}
}
const mapStateToProps = (state, ownProps) => {
return ({
testList: state.test && state.test.items,
PreferredTest: state.test && state.test.PreferredTest
})
}
...
There are few different things that are wrong there.
Syntax error is because your <key> tag is not properly closed here:
(dataQuery) => <key={dataQuery.entityId}>{dataQuery})
And... there is no <key> element for React Native. You can check at docs Components section what components are supported. Btw there is no such an element for React also.
Requesting data is async. So when you send request in render() this method finishes execution much earlier before data is returned. You just cannot do that way. What can you do instead? You should request data(in this element or its parent or Redux reducer - it does not matter) and after getting results you need to set state with .setState(if it happens inside the component) or .dispatch(if you are using Redux). This will call render() and component will be updated with data retrieved. There is additional question about displaying spinner or using other approach to let user know data is still loading. But it's orthogonal question. Just to let you know.
Even if requesting data was sync somehow(for example reading data from LocalStorage) you must not ever do this in render().This method is called much more frequently that you can expect so making anything heavy here will lead to significant performance degradation.
So having #3 and #4 in mind you should run data loading/fetching in componentDidMount(), componentDidUpdate() or as a part of handling say button click.

Categories

Resources