Message Feed not Updating when Next Texts are Sent React Native - react-native

I have a pretty simple messaging component, all of the mutations and queries involved work properly. If I send a message, the mutation fires, and then the updated conversation is pulled from a query and changes the local state value of currentChat to the newest version of that chat. The query works, as shown by console.log statements, but the screen does not re-render or update at all. If I leave the page and come back to it, it is now updated. Similarly, if I hit ctrl-s in my IDE, the page refreshed and the expected messages were displayed. I use useEffects to mark a change in currentChatroom and all of the rendering functions are based off of values in that state, so I am unsure why it is not updating the visuals when the data has been updated
EDIT: Adding some code for clarity, the functions are all pretty huge (mostly styling logic) so I'm gonna show the barebones of it.
const [msgs, setMsgs] = useRecoilState(messageDataState)
// RENDERINGS //
// Renders the Next Upcoming Meetings
function renderUpcomingMeetings(){
if (msgs.length < 1){ // no meetings
return(
<View style={Styles.upcomingView}>
<Text style={{...FONTS.Title, color: COLORS.iconLight, textAlign: 'center', margin: 10}}>
You have no messages yet!
</Text>
</View>
)
}
return(
<ScrollView style={{...Styles.upcomingView, maxHeight: 350}}>
{renderMsgs()}
</ScrollView>
)
}
// Renders each meeting card
function renderMsgs(){
return msgs.filter(msg => {
if (!msg.sent && msg.deleted){
return null
}
}).map(meeting => {
let dt = convertDateTimeToJavaScript(msg.sentTime)
return(
// A bunch of irrelevant styling stuff
)
})
}
// SENDINGS //
// Sends message
async function handleAssignSubmissionClick(){
setLoading(true)
sendMessage({client: client, sDate: sDate})
.then((resolved) => {
setRefresh(!refresh)
})
}
// Triggers the Requery when refresh is changed post mutation
useEffect(() => {
getAndSetUser()
setLoading(false)
}, [refresh])
// Gets the user obj and resets the userState
async function getAndSetUser(){
await client.query({
query: GET_CHAT,
fetchPolicy: 'network-only'
})
.then(async (resolved) => {
await setMsgs(resolved.data.getChat.messages)
})
.catch((error) => {
console.log(error)
})
}

Related

Do I need Pagination, Lazy Loading or none in my React-Native app?

I am implementing an app in React-Native where I am fetching "restaurants" as documents from Cloud-Firestore and I am also using onSnapshot() listener. When the app is ready for launch, there will probably be around max 3000 restaurants. I have a few questions to ask around this matter.
-Do I need to implement Pagination/Lazy Loading for a better UX and less cost OR 3000 is not a big number so it won't affect the performance that much?!
-If I do need to implement one of them, which one should I implement?
-If I dont need, then is there a way to compress the JSON data when fetching in React-Native so it saves space? And then decompress when requested by the user.
Can user search for a restaurant while lazy loading is implemented?
EDIT:
I managed to implement lazy-loading and its working perfectly, however, using snapshot() listener will make lazy-loading pointless but I must use it because I need to fetch on real-time new "restaurants" or "orders. So, what else can I use instead of snapshot()? Or maybe is there a way to still use snapshot() but with a small change to the code?
Second question: after the above problem is solved, am I able to implement the search for a restaurant? It seems quite tricky knowing that I am using lazy-loading.
componentDidMount() {
try {
// Cloud Firestore: Initial Query
this.retrieveData();
}
catch (error) {
console.log(error);
}
};
retrieveData = () => {
try {
this.setState({
loading: true
})
var initialQuery = firebase.firestore().collection('restaurants')
.orderBy('res_id')
.limit(this.state.limit)
//let documentSnapshots = await initialQuery.get();
initialQuery.onSnapshot((documentSnapshots => {
var All = documentSnapshots.docs.map(document => document.data());
var lastVisible = All[All.length - 1].res_id;
this.setState({
All: All,
lastVisible: lastVisible,
loading: false,
});
}));
}
catch (error) {
console.log(error);
}
};
retrieveMore = async () => {
try {
// Set State: Refreshing
this.setState({
refreshing: true,
});
// Cloud Firestore: Query (Additional Query)
var additionalQuery = await firebase.firestore().collection('restaurants')
.orderBy('res_id')
.startAfter(this.state.lastVisible)
.limit(this.state.limit)
// Cloud Firestore: Query Snapshot
var documentSnapshots = await additionalQuery.get();
// Cloud Firestore: Document Data
var All = documentSnapshots.docs.map(document => document.data());
// Cloud Firestore: Last Visible Document (Document ID To Start From For Proceeding Queries)
var lastVisible = All[All.length - 1].res_id;
// Set State
this.setState({
All: [...this.state.All, ...All],
lastVisible: lastVisible,
refreshing: false,
});
console.log('Retrieving additional Data', this.state.All);
}
catch (error) {
console.log(error);
}
};
// Render Header
renderHeader = () => {
try {
return (
<Text style={styles.headerText}>Items</Text>
)
}
catch (error) {
console.log(error);
}
};
// Render Footer
renderFooter = () => {
try {
// Check If Loading
if (this.state.loading) {
return (
<ActivityIndicator />
)
}
else {
return null;
}
}
catch (error) {
console.log(error);
}
};
render() {
return (
<SafeAreaView style={styles.container}>
<FlatList
// Data
data={this.state.All}
// Render Items
renderItem={({ item }) => (
<View style={styles.itemContainer}>
<Text>(ID: {item.res_id}) {item.rest_name} {item.rest_location}</Text>
</View>
)}
// Item Key
keyExtractor={(item, index) => String(index)}
// Header (Title)
ListHeaderComponent={this.renderHeader}
// Footer (Activity Indicator)
ListFooterComponent={this.renderFooter}
// On End Reached (Takes a function)
onEndReached={this.retrieveMore}
// How Close To The End Of List Until Next Data Request Is Made
onEndReachedThreshold={0.1}
// Refreshing (Set To True When End Reached)
refreshing={this.state.refreshing}
/>
</SafeAreaView>
)
}
Pagination and Lazy Loading are kind of the same thing in this case. You will be either switching pages or infinitely scrolling while fetching new data until there is no more data to fetch. In either case, you need it. Look here https://firebase.google.com/docs/firestore/query-data/query-cursors
As pagination is merely a way you query your data, it has no effect on how you can use this data in other ways, meaning sure you can search for a restaurant given your database design and security rules are set up right
Not sure if you should be using onSnapshot listener though, as it will return you entire collection every time something changes, which denies the whole point of using pagination
EDIT:
About real time updates: it depends on what do you want to fetch in real time. Is it only those restaurants you have loaded or all of them? Imagine you had 1.000.000 restaurants in your db, you sure wouldn't want to query for them all for real time updates. Refreshing only those that are currently loaded is pretty expensive operation in firestore as it requires canceling and creating new subscriptions every time your visible restaurants change (e.g. as you scroll through the page). So both variants are not an option
You should reconsider your app design - e.g. do you really need to live track orders of every restaurant registered? Maybe you only want those where you are an employee, or only those near you? Maybe you should restructure your database to keep orders separately from restaurants, that way you can listen for orders while still lazy loading restaurants?

How to remount a screen from another screen? (Refresh the whole app again with new parameters)

I have a configurable application which everything is fed into the app from a middleware (like colors and contents) based on a unique id so-called appId.
In the home screen, I am fetching all required data from a middleware in componentDidMount() function and then use it later on. For the first time, I am using a default appId and the componentDidMount() looks like this:
componentDidMount() {
this.setState({ isLoading: true });
fetch(
API +
"configurations" +
"?" +
"uuid=blabla" +
"&" +
"appId=" +
appId +
"&" +
"locale=" +
locale +
"&" +
"gid=" +
gid,
{
method: "GET",
headers: {
Accept: "application/json"
}
}
)}
I have another screen (settings screen) where I have a box and the user can insert appId as input.
When the appId is inserted by the user (in the settings page), I would like to navigate back to the Home screen and re-fetch the data with the new appId that was inserted by the user. The setting screen looks like this:
state = {
newappId: "" };
handlenewappId = text => {
this.setState({ newappId: text });
};
.....
<Item regular>
<Input
onChangeText={this.handlenewappId}
placeholder="Regular Textbox"
/>
<Button
onPress={() => {
navigation.navigate("Home");
}}
>
<Text>Save</Text>
</Button>
</Item>
However, when I do navigation.navigate("Home") the componentDidMount() is not triggered in order to fetch the data again from the middleware (which is expected since it is only triggered for the first time).
What should I do? What is the solution?
I have already tried the solution given in `componentDidMount()` function is not called after navigation
but it didn't work for me.
also tried to move the code in componentDidMount() into a separate function and call it from the settings page but I couldn't make it work.
============== UPDATE: ==============
I was able to solve the issue with the answer given by "vitosorriso" below. However, a new issue occurs. After fetching is done, I am pushing the response to the state and then use it my home screen like this:
fetchData = async () => {
this.setState({ isLoading: true }, async () => {
//fetch the data and push the response to state. e.g:
this.setState({ page: data, configs: data2, isLoading: false });
}}
....
render() {
const { configs, page, isLoading, error } = this.state; //getting the data fetched in the fetch function and pushed to the state
if (isLoading || !page || !configs) {
//if data is not ready yet
);
// Use the data to extract some information
let itemMap = page.item.reduce((acc, item) => {
acc[item.id] = item;
item.attributes = item.attributes.reduce((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {});
return acc;
}, {});
}}
For the first time the app starts, everything works fine and there is no error but if I go to the settings page and press the button to navigate back to the home screen and fetch data again, I face the error:
"items.attributes.reduce is not a function".
I am assuming the reason is, "items.attributes" already has a value (from the first time) and can't fed with new data again.
Is there any way, to clear all the variables when navigating from settings page to the home page?
I have solved the same problem in my app with a similar concept of this ( `componentDidMount()` function is not called after navigation ) but using a different syntax, and it is working for me:
// your home class
// no need to import anything more
// define a separate function to fetch data
fetchData = async () => {
this.setState({ isLoading: true }, async () => {
// fetch your data here, do not forget to set isLoading to false
}
}
// add a focus listener onDidMount
async componentDidMount () {
this.focusListener = this.props.navigation.addListener('didFocus', async () => {
try {
await this.fetchData() // function defined above
} catch (error) {
// handle errors here
}
})
}
// and don't forget to remove the listener
componentWillUnmount () {
this.focusListener.remove()
}

Refresh data when navigating back react native

How do I refresh my data when navigating back? For example, if I want to edit something and then press the "Back arrow", I want to immediately see the changes.
componentWillMount() {
getData("data", (res) => {
this.setState({ name: res.name, date: res.date});
var date = new moment(this.state.date);
var currentDate = new moment();
var duration = moment.duration(date.diff(currentDate)).as("days");
duration = Math.round(duration + 1).toString();
var daysLeftPercent = Math.round([duration / 340] * 100);
this.setState({ daysLeft: duration, daysLeftPercent: daysLeftPercent});
});
}
Like this but I also want to run it when pressing the "back arrow" on another page that leads to this.
If you want to access a component's method from another component, you should bind it(may within props or navigation params).
And to access 'back arrow'(i think you means devıce's back button otherwise its up to you) you can use BackHandler from react native.
BackHandler.addEventListener(
"hardwareBackPress",
this.props.onPressBackButton
);
...
onPressBackButton = () => {
//do something
}
You if you want to navigate back to a previous component and pass data to the previous component you can use () => this.props.navigation.goBack()
For example if you wanted to click a button to go back:
<TouchableHighlight onPress={() => this.props.navigation.goBack(
//do something here
)}>
<Text> Go Back to previous component </Text>
</TouchableHighlight>
How to Make the Screen Refresh when Navigating Back in React Native----
React.useEffect(() => { const unsubscribe = navigation.addListener('focus', () => { Alert.alert('Refreshed'); }); return unsubscribe; }, [navigation]);
use this into useefect and before render/return

React Native Real-Time Issue

I am fetching Data from an API to a flatlist and its working fine the problem it's not a real time fetching, I have to refresh the screen in order to see the recent added data. How can I make the fetching real time.
export default class CategoryScreen extends Component {
constructor(props) {
super(props);
this.state = {
categories: []
}
}
componentWillMount() {
//Fetch recent products
const url = url
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
this.setState({
categories: responseJson.categories
})
})
}
rederItem = ({ item }) => {
return (
<CategoryFlatList title={item.cat_name} image={item.img}/>
)
}
render() {
return (
<SafeAreaView style={{ flex: 1 }}>
<FlatList style={{}}
data={this.state.categories}
renderItem={this.rederItem}
numColumns={2}
keyExtractor={item => item.id}
/>
</SafeAreaView>
);
}
}
If I understand your problem correctly you need to refresh the data when it was changed on the server!
If this is the case, the reason why it did not update on the list cause you did not call fetch, when u refresh the screen the fetch get called and the data get updated.
There are multiple solutions to fix this problem:
An advanced one by creating a mechanism to subscribe to the data source and update when it is changed.
Update data on user interaction (like a swipe).
A simpler way (but not that professional) by using a timer that calls the fetch and updates the state.
These are the things you can do:
1) Make a PubSub mechanism
2) Use pull to refresh in your FlatList
3) Use polling, you can either do polling by using Redux-Sagas or Redux-Observable which is the preferred method or you can do it by using timer like this
This is how you start interval
this.intervalID = setInterval(() => {
// do something
}, 5000) //time for polling in millies
This is how you can stop interval
if (this.intervalID != null) {
clearInterval(this.intervalID)
this.intervalID = null
}

react-native-router-flux: How to return to previous Scene forcing reload of data?

I'm using a List View
class GroupList extends Component {
componentWillMount() {
this.props.fetchGroups();
I fetch from firebase a VERY SHORT list of elements.
export const fetchGroups = () => {
const { currentUser } = firebase.auth();
return (dispatch) => {
dispatch({
type: FETCHING_GROUPS
});
firebase.database().ref(`/users/${currentUser.uid}/groups`)
.on('value', snapshot => {
dispatch({
type: GROUPS_FETCHED,
payload: snapshot.val()
});
});
};
};
To create a new item I use the top-right Button offered from react-native-router-flux
<Scene
key="groupList"
component={GroupList}
title="Gruppi di servizio"
hideNavBar={false}
rightTitle="Nuovo"
onRight={() => Actions.groupForm({ formType: NEW_GROUP_FORM_TYPE })}
sceneStyle={{ paddingTop: 65 }}
/>
So I click top-right 'new' button and I see my form.
User then save the new group name
This is my action to Save to firebase:
return (dispatch) => {
dispatch({
type: TRYING_TO_CREATE_GROUP,
});
firebase.database().ref(`/users/${currentUser.uid}/groups`)
.push({ groupName })
.then(() => {
dispatch({ type: CREATE_GROUP_SUCCESS });
Actions.groupList({ type: 'reset' });
})
.catch((errorText) => {
dispatch({
type: CREATE_GROUP_FAILED,
payload: errorText
});
});
};
What's the problem?
After the first round of
Actions.groupForm({ formType: NEW_GROUP_FORM_TYPE })}
and
Actions.groupList({ type: 'reset' })
My GroupList is succesfully refreshed, because componentWillMount is called again.
The problem is that after the SECOND round of groupForm -> groupList my list is no more refreshed.
Adding a console log to componentWillMount I noticed that it's no more executed.
Why?
What's the right way to return to the Scene 'groupList' , forcing the refresh at any cost?
I solved this by doing the following
Actions.replace(sceneKey, props );
For your code
Actions.replace('groupForm', { formType: NEW_GROUP_FORM_TYPE } );
Hope this help!
This has worked for me. Try it.
The gist of it is when you define a Scene, you can override its "onBack" function. In order to force refresh of the previous page, you have to provide it with a prop with a different value each time, thus the Math.random().
<Scene
key='title'
component={YourComponent}
title='Title'
onBack={() => Actions.pop({ refresh: Math.random() })}
/>
I haven't worked with react-native-router-flux but I've run into the same problem and hopefully can give you enough to figure out how to do it with that library.
Basically It doesn't get called again (componentWillMount) because the component is already mounted and its just returning focus to that component. What you have to do is pass that fetchGroups() to wherever the next scene is and call it again once you're returning back to the GroupList scene which will cause a re-render with the most updated information. Let me know if I should clarify further.
Inspired by #Matt's solution, I solved my problem. And I am gonna post my sample code.
The idea is that if you wanna refresh the parent page while routing back from a child page, the way I do it is to pass a destroy and an update callback function to the child. The destroy function makes sure that the page falls back to its initial state, while the update function will fetch the latest data.
// parent scene
Actions.childSceneName({
onBack: () => this.props.fetchData(),
destroyParent: () => this.props.destroyPage(),
})
So that the child page can invoke the functions before routing back.
// child scene
componentWillUnmount() {
this.props.destroyParent()
this.props.onBack()
}
The best way is using a State Manager OR the new Context API of React.
This is the flow:
Entering into ScreenOne
From ScreenOne open ScreenTwo (both using state management)
When finishing all the actions in ScreenTwo, call your function that reloads ScreenOne, at the same time use Actions.pop() to leave current screen (leaving ScreenTwo, and getting back into ScreenOne).
Done! your ScreenOne is reloaded with new data.
Summary: The key of this is calling your function that reloads
Previous Screen.