I'm using Firestore to store messages. In order to optimize the mobile application performances, I would like to set a limit(50) in the firestore query.
It works well and implemented the onLoadEarlier React-native-gifted-chat available in the props.
All is working fine.
But, when I send a new message in the chat, after scrolled up to see the earliers messages, only the 50 last messages with the new one, off course, are available.
So, each time I'm adding a message in the Firestore database, the onSnapshot (in the useeffect) is executed and apply the limit query.
Is there a way to avoid this ?
Thanks.
Here my useEffect :
useEffect(() => {
const messagesListener = firestore()
.collection('groups')
.doc(group._id)
.collection('messages')
.orderBy('createdAt', 'desc')
.limit(50)
.onSnapshot(querySnapshot => {
const newMessages = querySnapshot.docs.map(doc => {
const firebaseData = doc.data();
const data = {
_id: doc.id,
text: '',
createdAt: new Date().getTime(),
...firebaseData
};
return data;
});
setMessages(previousMessages => {
return GiftedChat.append(previousMessages, newMessages);
});
});
return () => messagesListener();
}, []);
I am using FlatList in react-native to render chats and I had to paginate the chats list. Since Firestore query cursor is not supported in live listener, I created two list, recentChats & oldChats.
I populate recentChats using live listener query.onSnapshot & oldChats using cursor startAfter. FlatList data is combination of both list and I take care of merging logic.
const MESSAGE_LIMIT = 15;
const ChatWindow = props => {
const { sessionId, postMessage, onSendTemplateButtonPress } = props;
// Firestore cursor is not supported in query.onSnapshot so maintaining two chat list
// oldChats -> chat list via cursor, recentChats -> chat list via live listener
const [oldChats, setOldChats] = useState([]);
const [recentChats, setRecentChats] = useState([]);
// if true, show a loader at the top of chat list
const [moreChatsAvailable, setMoreChatsAvailable] = useState(true);
const [inputMessage, setInputMessage] = useState('');
useEffect(() => {
const query = getGuestChatMessagesQuery(sessionId)
.limit(MESSAGE_LIMIT);
const listener = query.onSnapshot(querySnapshot => {
let chats = [];
querySnapshot.forEach(snapshot => {
chats.push(snapshot.data());
});
// merge recentChats & chats
if (recentChats.length > 0) {
const newRecentChats = [];
for (let i = 0; i < chats.length; i++) {
if (chats[i].sessionId === recentChats[0].sessionId) {
break;
}
newRecentChats.push(chats[i]);
}
setRecentChats([...newRecentChats, ...recentChats]);
} else {
setRecentChats(chats);
if (chats.length < MESSAGE_LIMIT) {
setMoreChatsAvailable(false);
}
}
});
return () => {
// unsubscribe listener
listener();
};
}, []);
const onMessageInputChange = text => {
setInputMessage(text);
};
const onMessageSubmit = () => {
postMessage(inputMessage);
setInputMessage('');
};
const renderFlatListItem = ({ item }) => {
return (<ChatBubble chat={item} />);
};
const onChatListEndReached = () => {
if (!moreChatsAvailable) {
return;
}
let startAfterTime;
if (oldChats.length > 0) {
startAfterTime = oldChats[oldChats.length - 1].time;
} else if (recentChats.length > 0) {
startAfterTime = recentChats[recentChats.length - 1].time;
} else {
setMoreChatsAvailable(false);
return;
}
// query data using cursor
getGuestChatMessagesQuery(sessionId)
.startAfter(startAfterTime)
.limit(MESSAGE_LIMIT)
.get()
.then(querySnapshot => {
let chats = [];
querySnapshot.forEach(snapshot => {
chats.push(snapshot.data());
});
if (chats.length === 0) {
setMoreChatsAvailable(false);
} else {
setOldChats([...oldChats, ...chats]);
}
});
};
return (
<View style={[GenericStyles.fill, GenericStyles.p16]}>
<FlatList
inverted
data={[...recentChats, ...oldChats]}
renderItem={renderFlatListItem}
keyExtractor={item => item.messageId}
onEndReached={onChatListEndReached}
onEndReachedThreshold={0.2}
ListFooterComponent={moreChatsAvailable ? <ActivityIndicator /> : null}
/>
{
Singleton.isStaff ?
null:
<ChatInput
onMessageInputChange={onMessageInputChange}
onMessageSubmit={onMessageSubmit}
inputMessage={inputMessage}
style={GenericStyles.selfEnd}
onSendTemplateButtonPress={onSendTemplateButtonPress}
/>
}
</View>
);
};
Your query is OK for the first time, for consequent queries you must use the ::startAt or ::startAfter methods.
You can find more information in the official documentation.
https://firebase.google.com/docs/firestore/query-data/query-cursors
Related
I have run into the firebase “IN” limit of 10. Although a workaround solution was already answered here:
Is there a workaround for the Firebase Query "IN" Limit to 10?
None of the solutions in that thread seem to work with the listener “onSnapshot”. For my use case (Vue 3), I have a composable/function call I that queries firebase passing in an array that could have up to 100 document ID values and returns an object as below.
Is this possible?
import { ref, watchEffect } from 'vue'
import { db } from '#/firebase/config'
import { collection, onSnapshot, query, where, documentId } from 'firebase/firestore'
const getUsersList = (idList) => {
// idList is an array of document ID's
const documents = ref(null)
let collectionRef = collection(db, 'users')
collectionRef = query(collectionRef, where(documentId(), 'in', idList))
// this fails if I pass in more than 10 elements in the array
const unsub = onSnapshot(collectionRef, snapshot => {
let results = []
snapshot.docs.forEach(doc => {
results.push({ ...doc.data(), id: doc.id })
})
// update values
documents.value = results
})
watchEffect((onInvalidate) => {
onInvalidate(() => unsub())
})
return { documents }
}
export default getCollectionRt
Since no replies here completely answered the question, I ended up paying a freelancer to take a look and here's what they came up with. The solution does seem to have a random issue I am trying to sort out when the underlying changes, one of the records will disappear. It does work, is in scope of the original question and seems to have solved the problem.
import { ref, watchEffect } from 'vue'
import { db } from '#/firebase/config'
import { collection, onSnapshot, query, where, documentId } from 'firebase/firestore'
const getUserList = (idList) => {
console.log('idList', idList)
let documents = ref(null)
let collectionRef = collection(db, 'users')
let unsub, unsubes = [], resultsList = [{}];
for (let i = 0; i < idList.length; i += 10) {
let idList1 = idList.slice(i, i + 10); //console.log(idList1);
let collectionRef1 = query(collectionRef, where(documentId(), 'in', idList1))
unsub = onSnapshot(collectionRef1, snapshot => {
let results = []
snapshot.docs.forEach(doc => {
results.push({ ...doc.data(), id: doc.id })
})
resultsList.splice(resultsList.length, 0, ...results);
console.log('results', results)
documents.value = results
})
unsubes.push(unsub);
}
watchEffect((onInvalidate) => {
onInvalidate(() =>{ unsubes.forEach(unsub => { unsub(); console.log("unsut", unsub); }) });
})
Promise.all(unsubes);
resultsList.shift(0);
console.log("docu", documents.value);
return { documents };
}
export default getUserList
You will have to initialize multiple listeners i.e. same number of queries but with onSnapshot() (might be better than setting up a listener for each individual document). Try:
import { ref } from 'vue';
import { collection, query, where, documentId, onSnapshot } from 'firebase/firestore'
const users = ref({})
const dataLoaded = ref(false)
const addFirestoreListeners = async () => {
const idList = [];
for (let i = 0; i < idList.length; i += 10) {
const items = idList.slice(i, i + 10)
const q = query(collection(db, 'users'), where(documentId(), 'in', items))
onSnapshot(q, (snapshot) => {
if (dataLoaded.value) {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added' || change.type === 'modified') {
users.value[change.doc.id] = change.doc.data()
} else (change.type === 'removed') {
users.value[change.doc.id] = null
}
})
} else {
snapshot.forEach((doc) => {
users.value[doc.id] = doc.data()
})
dataLoaded.value = true
}
})
}
}
The dataLoaded flag checks if the listeners have fetched data for first time or has received an update. I've use a map where the key is document ID so it can be removed easily but do change that to an array or any other required structure.
I am trying to delete a document from firebase firestore collection
how can I do that , I tried few things but no success
here is my code
here is the Firestore hook
const useFirestore = (somecollection) => {
const [docs, setDocs] = useState([]);
useEffect (() => {
// new collection reference
const newcoll = collection(projectFirestore, somecollection);
const q = query(newcoll, orderBy('createdAt', 'desc'));
const unsub = onSnapshot(q, (snapshot) => {
let documents = [];
snapshot.forEach(doc => {
documents.push({...doc.data(), id: doc.id})
})
setDocs(documents);
});
return () => unsub;
},[somecollection]);
return { docs };
}
the imagegrid where all the images are shown (only the js without the jsx return)
const { docs } = useFirestore('images');
const handleDelete = (e) => {
docs.forEach(doc => {
deleteDoc(doc);
})
now , on the imagegrid jsx I have all the uploaded images that store the document in them.
so I added a button to every image and I want that when I click the button its fires a handleDelete() that delete this specific image document from the firestore
If you are trying to have a button that deletes a single document, you likely do NOT want to be looping over all of the docs and calling deleteDoc().
Instead, your UI would likely display each image and include its ID for each button. Something like:
const deleteOneImageDoc = async (docId) => {
try {
let docRef = doc(myFirestore, `images/${docId}`);
await deleteDoc(docRef);
} catch (ex) {
console.error(`Delete FAILED: ${ex.message}`);
throw ex;
}
};
return <div>
docs.map((imgDoc) => {
return (
<div key={imgDoc.id}>
<img src={imgDoc.url}/>
<button onClick={() => deleteOneImageDoc(imgDoc.id)}>
delete {imgDoc.id}
</button>
</div>);
})
</div>;
I have used following code in my component to load new data when scrolling.But when new page of data is loaded the scroll bar keeps returning to the top.I'm using this for Magento PWA with react.
const Category = props => {
const {id} = props;
const classes = mergeClasses(defaultClasses, props.classes);
const [paginationValues, paginationApi] = usePagination();
const {currentPage, totalPages} = paginationValues;
const {setCurrentPage, setTotalPages} = paginationApi;
const [selectedPageSize, setPageSize] = useState(40);
const [pageNumber, setPageNumber] = useState(1);
function handlePageSize() {
setPageNumber(pageNumber+1)
}
const sortProps = useSort();
const [currentSort] = sortProps;
const previousSort = useRef(currentSort);
const pageControl = {
currentPage,
setPage: setCurrentPage,
totalPages
};
const [runQuery, queryResponse] = useLazyQuery(GET_CATEGORY,{fetchPolicy:'cache-first'});
const {loading, error, data} = queryResponse;
const {search} = useLocation();
const bqueryResponsee = useQuery(
GET_ATTRIBUTES
);
let battributedata = '';
// Keep track of the search terms so we can tell when they change.
const previousSearch = useRef(search);
// Get "allowed" filters by intersection of schema and aggregations
const {data: introspectionData} = useQuery(FILTER_INTROSPECTION);
// Create a type map we can reference later to ensure we pass valid args
// to the graphql query.
// For example: { category_id: 'FilterEqualTypeInput', price: 'FilterRangeTypeInput' }
const filterTypeMap = useMemo(() => {
const typeMap = new Map();
if (introspectionData) {
introspectionData.__type.inputFields.forEach(({name, type}) => {
typeMap.set(name, type.name);
});
}
return typeMap;
}, [introspectionData]);
// Run the category query immediately and whenever its variable values change.
useEffect(() => {
window.addEventListener('scroll', infiniteScroll);
// Wait until we have the type map to fetch product data.
if (!filterTypeMap.size) {
return;
}
const filters = getFiltersFromSearch(search);
const newFilters = {};
newFilters['category_id'] = {eq: String(id)};
filters.forEach((values, key) => {
newFilters[key] = getFilterInput(values, filterTypeMap.get(key));
});
runQuery({
variables: {
currentPage: Number(pageNumber),
id: Number(id),
filters: newFilters,
pageSize: Number(selectedPageSize),
sort: {[currentSort.sortAttribute]: currentSort.sortDirection}
}
});
window.scrollTo({
left: 0,
top: 0,
behavior: 'smooth'
});
}, [
currentPage,
currentSort,
filterTypeMap,
id,
selectedPageSize,
runQuery,
search,
pageNumber
]);
const totalPagesFromData = data
? Math.ceil(data.category.product_count/selectedPageSize)
: null;
useEffect(() => {
setTotalPages(totalPagesFromData);
return () => {
setTotalPages(null);
};
}, [setTotalPages, totalPagesFromData]);
// If we get an error after loading we should try to reset to page 1.
// If we continue to have errors after that, render an error message.
useEffect(() => {
if (error && !loading && currentPage !== 1) {
setCurrentPage(1);
}
}, [currentPage, error, loading, setCurrentPage]);
// Reset the current page back to one (1) when the search string, filters
// or sort criteria change.
useEffect(() => {
// We don't want to compare page value.
const prevSearch = new URLSearchParams(previousSearch.current);
const nextSearch = new URLSearchParams(search);
prevSearch.delete('page');
nextSearch.delete('page');
if (
prevSearch.toString() !== nextSearch.toString() ||
previousSort.current.sortAttribute.toString() !==
currentSort.sortAttribute.toString() ||
previousSort.current.sortDirection.toString() !==
currentSort.sortDirection.toString()
) {
// The search term changed.
setCurrentPage(1);
// And update the ref.
previousSearch.current = search;
previousSort.current = currentSort;
}
}, [currentSort, previousSearch, search, setCurrentPage]);
if (error && currentPage === 1 && !loading) {
if (process.env.NODE_ENV !== 'production') {
console.error(error);
}
return <div>Data Fetch Error</div>;
}
// Show the loading indicator until data has been fetched.
if (totalPagesFromData === null) {
return fullPageLoadingIndicator;
}
if (typeof bqueryResponsee.data !== "undefined" && !bqueryResponsee.loading) {
battributedata = bqueryResponsee.data.customAttributeMetadata.items[0].attribute_options;
} else {
battributedata = [];
}
const count = totalPagesFromData ? totalPagesFromData : null;
const galleries = [];
if (count && count >= 1) {
for (let i =1;i<=pageNumber;i++) {
galleries.push(
<section className={classes.gallery}>
<Gallery searchItems={null} ID={1}
newSort={{[currentSort.sortAttribute]: currentSort.sortDirection}}
newFilters={newFiltersLazy}
currentPage={i} categoryId={id} pageSize={40} introspectionData={introspectionData}
battributedata={battributedata}/>
</section>
)
}
}
function infiniteScroll(){
console.log(Math.ceil((window.innerHeight + document.documentElement.scrollTop)/100)*100,Math.ceil((document.documentElement.offsetHeight*8/10)/100)*100)
// End of the document reached?
if ( Math.ceil((window.innerHeight + document.documentElement.scrollTop)/100)*100
>= Math.ceil((document.documentElement.offsetHeight*8/10)/100)*100){
handlePageSize()
}
}
return (
<Fragment>
<Meta name="description" content={metaDescription}/>
<CategoryContent
totalPagesFromData={totalPagesFromData}
categoryId={id}
classes={classes}
data={loading ? null : data}
pageControl={pageControl}
sortProps={sortProps}
onSelectSize={handlePageSize}
battributedata={battributedata}
selectedPageSize={selectedPageSize}
galleries={galleries}
newSort={{[currentSort.sortAttribute]: currentSort.sortDirection}}
newFilters={newFiltersLazy}
introspectionData={introspectionData}
pageNumber={pageNumber}
/>
</Fragment>
);
};
What I did here is created a new array galleries and the Gallery component in each page increment while scrolling and inside Gallery the items will be created.Problem is the scroll bar keep returning to the top in each increment.Please help
I'm trying to make a list in my react app. I have retrieved data from my database, and pushed it into a list. I have doublechecked that the data shows up correctly in the console, and it does, but array.map() returns nothing. I think the problem might be that array.map() runs two times. I don't know why it runs two times.
function Dashboard() {
const user = firebase.auth().currentUser;
const [teams, setTeams] = useState([])
const history = useHistory();
useEffect(() => {
getTeams()
if (user) {
} else {
history.push("/")
}
}, [])
function Welcome() {
if (user) {
return <h1>Welcome, {user.displayName}</h1>
} else {
}
}
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = []
docList.forEach((data) => {
firebase.firestore().collection('teams').doc(data.teamId).get().then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
})
setTeams(teamslist)
})
}
const openTeam = (data) => {
console.log(data.teamId)
}
return (
<div>
<Welcome />
<div>
<ul>
{console.log(teams)}
{teams.map((data) => {
return (
<li onClick={() => openTeam(data)} key={data.teamId}>
<h1>{data.name}</h1>
<p>{data.teamId}</p>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Dashboard
The getTeams function has a bug where it isn't waiting for the firebase.firestore().collection('teams').doc(data.teamId).get().then promises to finish before calling setTeams, so it is called with an empty array, causing React to trigger a render with the empty array.
As the promises for fetching each team resolve they will be pushed to the same array reference, but this won't trigger a rerender in React since you're not calling setTeams again when the array changes.
Try this code, which won't call setTeams until each team promise generated from docList has been resolved.
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = [];
Promise.all(docList.map((data) => {
return firebase
.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
}))
.then(() => setTeams(teamslist));
})
}
A smaller edit would be to call setTeams after each separate team promise resolves, which will trigger a React render each time a new team is resolved:
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
});
// create a new array, since using the same array
// reference won't cause react to rerender
setTeams([...teamslist]);
})
Many thanks to #martinstark who provided you an answer while I was unavailable.
However, there are some more things that need to be covered.
User State
In your current component, you pull the current user from Firebase Authentication, but don't handle the state changes of that user - signing in, signing out, switching user. If a user is signed in and they were to navigate directly to your dashboard, firebase.auth().currentUser could be momentarily null while it resolves the user's login state, which would incorrectly send them off to your login page.
This can be added using:
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
Next, in your first useEffect call, you call getTeams() whether the user is signed in or not - but it should depend on the current user.
useEffect(() => {
if (userLoading) {
return; // do nothing (yet)
} else if (user === null) {
history.push("/");
return;
}
getTeams()
.catch(setError);
}, [user]);
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const docList = []
membersQuerySnapshot.forEach((doc) => {
docList.push({
teamId: doc.get("teamId"), // better perfomance than `doc.data().teamId`
});
});
const teamDataList = [];
await Promise.all(docList.map((data) => {
return firebase.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then(doc => teamDataList.push({
name: doc.get("name"),
teamId: doc.id
}));
}));
setTeams(teamDataList);
}
Optimizing getTeams() - Network Calls
The getTeams function in your question calls setTeams with the array [], which will be empty at the time of calling it as covered in #martinstark's answer. The "get team data" operations are asyncronous and you aren't waiting for them to resolve before updating your state and triggering a new render. While you are pushing data to them after the component has rendered, modifying the array won't trigger a new render.
While you could fetch the data for each team using db.collection("teams").doc(teamId).get(), each of these is requests is a network call, and you can only make a limited number of these in parallel. So instead of fetching 1 team per network call, you could fetch up to 10 teams per network call instead using the in operator and FieldPath.documentId().
Assuming the collectionGroup("members") targets the collections of documents at /teams/{aTeamId}/members which contain (at least):
"/teams/{aTeamId}/members/{memberUserId}": {
teamId: aTeamId,
user: memberUserId, // if storing an ID here, call it "uid" or "userId" instead
/* ... */
}
// this utility function lives outside of your component near the top/bottom of the file
function chunkArr(arr, n) {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const chunkedTeamIDList = chunkArr(teamIDList, 10) // split into batches of 10
const teamsColRef = firebase.firestore().collection('teams');
const documentId = firebase.firestore.FieldPath.documentId(); // used with where() to target the document's ID
const foundTeamDocList = await Promise
.all(chunkedTeamIDList.map((chunkOfTeamIDs) => {
// fetch each batch of IDs
return teamsColRef
.where(documentId, 'in', chunkOfTeamIDs)
.get();
}))
.then((arrayOfQuerySnapshots) => {
// flatten results into a single array
const allDocsList = [];
arrayOfQuerySnapshots.forEach(qs => allDocsList.push(...qs.docs));
return allDocsList;
});
const teamDataList = foundTeamDocList
.map((doc) => ({ name: doc.get("name"), teamId: doc.id }));
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
You can also make use of this utility function to simplify & optimize the code a bit. Which gives:
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
Optimizing getTeams() - Function Definition
As part of the last optimization, you could pull it out of your component or place it in its own file so that it's not redefined with every render:
// define at top/bottom of the file outside your component
// This getTeams() is a (userId: string) => Promise<{ name: string, teamId: string}[]>
async function getTeams(userId) => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', userId)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// return the sorted teams
return teamDataList
}
and update how you use it:
useEffect(() => {
if (userLoading) {
return; // do nothing
} else if (user === null) {
history.push("/");
return;
}
getTeams(user.uid)
.then(setTeams)
.catch(setError);
}, [user]);
My FlatList does not update when the props I pass from redux change. Every time I send a message I increase everyones unread message count in both firebase and in my redux store. I made sure to include key extractor and extra data, but neither helps. The only thing that changes the unread message count is a reload of the device. How do I make sure the flatList updates with MapStateToProps. I made sure to create a new object by using Object.Assign:
action:
export const sendMessage = (
message,
currentChannel,
channelType,
messageType
) => {
return dispatch => {
dispatch(chatMessageLoading());
const currentUserID = firebaseService.auth().currentUser.uid;
let createdAt = firebase.database.ServerValue.TIMESTAMP;
let chatMessage = {
text: message,
createdAt: createdAt,
userId: currentUserID,
messageType: messageType
};
FIREBASE_REF_MESSAGES.child(channelType)
.child(currentChannel)
.push(chatMessage, error => {
if (error) {
dispatch(chatMessageError(error.message));
} else {
dispatch(chatMessageSuccess());
}
});
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child(channelType)
.child(currentChannel).child('users')
UNREAD_MESSAGES.once("value")
.then(snapshot => {
snapshot.forEach(user => {
let userKey = user.key;
// update unread messages count
if (userKey !== currentUserID) {
UNREAD_MESSAGES.child(userKey).transaction(function (unreadMessages) {
if (unreadMessages === null) {
dispatch(unreadMessageCount(currentChannel, 1))
return 1;
} else {
alert(unreadMessages)
dispatch(unreadMessageCount(currentChannel, unreadMessages + 1))
return unreadMessages + 1;
}
});
} else {
UNREAD_MESSAGES.child(userKey).transaction(function () {
dispatch(unreadMessageCount(currentChannel, 0))
return 0;
});
}
}
)
})
};
};
export const getUserPublicChannels = () => {
return (dispatch, state) => {
dispatch(loadPublicChannels());
let currentUserID = firebaseService.auth().currentUser.uid;
// get all mountains within distance specified
let mountainsInRange = state().session.mountainsInRange;
// get the user selected mountain
let selectedMountain = state().session.selectedMountain;
// see if the selected mountain is in range to add on additional channels
let currentMountain;
mountainsInRange
? (currentMountain =
mountainsInRange.filter(mountain => mountain.id === selectedMountain)
.length === 1
? true
: false)
: (currentMountain = false);
// mountain public channels (don't need to be within distance)
let currentMountainPublicChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Public");
// mountain private channels- only can see if within range
let currentMountainPrivateChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Private");
// get public channels
return currentMountainPublicChannelsRef
.orderByChild("key")
.once("value")
.then(snapshot => {
let publicChannelsToDownload = [];
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
// add the channel ID to the download list
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
// flag whether you can check in or not
if (currentMountain) {
dispatch(checkInAvailable());
} else {
dispatch(checkInNotAvailable());
}
// if mountain exists then get private channels/ if in range
if (currentMountain) {
currentMountainPrivateChannelsRef
.orderByChild("key")
.on("value", snapshot => {
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",
snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
});
}
return publicChannelsToDownload;
})
.then(data => {
setTimeout(function () {
dispatch(loadPublicChannelsSuccess(data));
}, 150);
});
};
};
Reducer:
case types.UNREAD_MESSAGE_SUCCESS:
const um = Object.assign(state.unreadMessages, {[action.info]: action.unreadMessages});
return {
...state,
unreadMessages: um
};
Container- inside I hook up map state to props with the unread messages and pass to my component as props:
const mapStateToProps = state => {
return {
publicChannels: state.chat.publicChannels,
unreadMessages: state.chat.unreadMessages,
};
}
Component:
render() {
// rendering all public channels
const renderPublicChannels = ({ item, unreadMessages }) => {
return (
<ListItem
title={item.info.Name}
titleStyle={styles.title}
rightTitle={(this.props.unreadMessages || {} )[item.id] > 0 && `${(this.props.unreadMessages || {} )[item.id]}`}
rightTitleStyle={styles.rightTitle}
rightSubtitleStyle={styles.rightSubtitle}
rightSubtitle={(this.props.unreadMessages || {} )[item.id] > 0 && "unread"}
chevron={true}
bottomDivider={true}
id={item.Name}
containerStyle={styles.listItemStyle}
/>
);
};
return (
<View style={styles.channelList}>
<FlatList
data={this.props.publicChannels}
renderItem={renderPublicChannels}
keyExtractor={(item, index) => index.toString()}
extraData={[this.props.publicChannels, this.props.unreadMessages]}
removeClippedSubviews={false}
/>
</View>
);
}
}
Object.assign will merge everything into the first object provided as an argument, and return the same object. In redux, you need to create a new object reference, otherwise change is not guaranteed to be be picked up. Use this
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});
// or
const um = {...state.unreadMessages, [action.info]: action.unreadMessages }
Object.assign() does not return a new object. Due to which in the reducer unreadMessages is pointing to the same object and the component is not getting rerendered.
Use this in your reducer
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});