How to combine two Redux Firebase Cloud Firestore queries?

Viewed 453

I have two redux queries that pull posts from my Firebase Firestore. The first successfully displays all of the posts of the people I'm following:

export function fetchUsersFollowingPosts(uid) {
    return ((dispatch, getState) => {
        firebase.firestore()
            .collection("posts")
            .doc(uid)
            .collection("userPosts")
            .orderBy("creation", "asc")
            .get()
            .then((snapshot) => {
                const uid = snapshot.query.EP.path.segments[1];
                const user = getState().usersState.users.find(el => el.uid === uid);


                let posts = snapshot.docs.map(doc => {
                    const data = doc.data();
                    const id = doc.id;
                    return { id, ...data, user }
                })

                for (let i = 0; i < posts.length; i++) {
                    dispatch(fetchUsersFollowingLikes(uid, posts[i].id))
                }
                dispatch({ type: USERS_POSTS_STATE_CHANGE, posts, uid })

            })
    })
}

The second shows all of my own posts.

export function fetchUserPosts() {
    return ((dispatch) => {
        firebase.firestore()
            .collection("posts")
            .doc(firebase.auth().currentUser.uid)
            .collection("userPosts")
            .orderBy("creation", "desc")
            .get()
            .then((snapshot) => {
                let posts = snapshot.docs.map(doc => {
                    const data = doc.data();
                    const id = doc.id;
                    return { id, ...data }
                })
                dispatch({ type: USER_POSTS_STATE_CHANGE, posts })
            })
    })
}

Here's where I currently list the users from the people I follow. But how do I combine them so I can show both my posts and those of the people that I'm following in a single FlatList?

function Feed(props) {
    useStatusBar('dark-content');
    const [posts, setPosts] = useState([]);
    const [refreshing, setRefreshing] = useState(false)

    useEffect(() => {
        if (props.usersFollowingLoaded == props.following.length && props.following.length !== 0) {
            props.feed.sort(function (x, y) {
                return y.creation.toDate() - x.creation.toDate();
            })

            setPosts(props.feed);
            setRefreshing(false)
        }

    }, [props.usersFollowingLoaded, props.feed])

    
    return (
        <View style={styles.background}>
             {posts.length > 0 ?
            <View style={styles.containerGallery}>
                <FlatList
                    refreshControl={
                        <RefreshControl
                            refreshing={refreshing}
                            tintColor="white"
                            onRefresh={() => {
                                setRefreshing(true);
                                props.reload()
                            }}
                        />
                    }
                    showsVerticalScrollIndicator={false}
                    numColumns={1}
                    horizontal={false}
                    data={posts}
                    renderItem={({ item }) => (
                        <View style={styles.containerImage}>
                            <Card title={item.title} onPress={() => props.navigation.navigate(routes.GOOD_STUFF_DETAIL, { item: item, postId: item.id, uid: item.user.uid, user: item.user,})} showLike={true} author={"Recommended by " + item.user.name} likeItem={item} likeCount={item.likesCount} icon={categories.categories[item.categoryID].icon} timeStamp={timeDifference(new Date(), item.creation.toDate())}/>
                        </View>
                    )}
                />
                
            </View>
            : <NothingHere title="Follow friends" text="To see their Good Stuff here" icon="search" color="white"/> }
        </View>

    )
}
const mapStateToProps = (store) => ({
    currentUser: store.userState.currentUser,
    following: store.userState.following,
    feed: store.usersState.feed,
    usersFollowingLoaded: store.usersState.usersFollowingLoaded,
})

const mapDispatchProps = (dispatch) => bindActionCreators({ reload }, dispatch);

export default connect(mapStateToProps, mapDispatchProps)(Feed);

Below is my data structure:

Firestore data structure 1

Firestore data structure 2

Firestore likes substructure

Thanks for reading!

2 Answers

Maybe I misunderstood the database structure but AFAIK it doesn't seem possible to combine both queries. From what I see of the database structure you want to retrieve the posts of userB where you (userA) gave a "like". In order to get that information you scan across the path posts/{userUID}/userPosts/{docId}/likes. Given the queries scan different collection ranges I don't see a way to mix them.

Separately, let's assume for the sake of the argument that you already have a list containing the user UIDs of the people you follow. Then, the Firestore query feature that gets closer to the desired behavior are Collection Group queries. Having in mind the structure of the database:

posts
  uid1
    userPosts
      doc1
      doc2
  uid2
    userPosts
      docX
      docY

Essentially, collection group queries are a way to simultaneously query across all userPosts collections at once. If each document were to have the author ID as a field you would be able to do something like this:

db.collectionGroup("userPosts").where("uid", "in", ListOfFollowedUsers)

This won't totally solve the problem because the in operator clause is limited to 10 values, so you may apply it at most for 10 followed users.

Overall I would suggest to keep the queries separated and merge them in the application code.

Based on your data, and knowing for a fact how limited queries on Firestore are, you need to do that merge on the client.

What I would do is to keep the list on redux and handle the merge on the reducer. You just need to listen to both actions and then merge them as an array on your app.

If you want to avoid the user seeing partial data (eg, you show your own posts and then another refresh just adds your followers post thus the UI will change) you might want to keep two flags (booleans) while you are loading data and show a spinner if both lists haven't been loaded.

Unless you want to refactor your code, then a lot of merging happens on the client.

Also, side node, I would NOT dispatch something on a for loop like you are doing on the first one, because if that triggers a firestore request, then it might get expensive real fast.

Related