简体   繁体   English

React Native Flatlist 搜索从 firebase 用户集合中返回没有值

[英]React Native Flatlist search returning no values from firebase users collection

so I recently tried to make a FlatList search for users in firebase, but I have been running into a bunch of errors, although there seems to be no bugs in the code.所以我最近尝试在 firebase 中对用户进行 FlatList 搜索,但是我遇到了一堆错误,尽管代码中似乎没有错误。 At the moment, the list searches and does not return anything, although there is clearly data in the firebase collection of "users".目前,列表搜索并没有返回任何内容,尽管“用户”的 firebase 集合中有明显的数据。 When I try to log "results" right above the resolve statement of the Promise in getUsers(), I all of a sudden see users, although I get the error that "results" does not exist, which is strange because why does the error make the code work?当我尝试在 getUsers() 中 Promise 的解析语句正上方记录“结果”时,我突然看到用户,虽然我收到“结果”不存在的错误,这很奇怪,因为为什么错误使代码工作? Anyways, If anyone would be able to help me in trying to make this FlatList work, I would greatly appreciate it.无论如何,如果有人能够帮助我尝试使这个 FlatList 工作,我将不胜感激。 I have been working on this for 3 days now and can't seem to find any solution online or fix the code.我已经为此工作了 3 天,似乎无法在线找到任何解决方案或修复代码。 For your help, I would gladly venmo you a dunkin donut, as this means a lot to me.为了你的帮助,我很乐意给你一个邓肯甜甜圈,因为这对我来说意义重大。 I appreciate all help and tips, and thank you in advance for your time!感谢所有帮助和提示,并提前感谢您的宝贵时间! (The code for my flatlist is below without the styles) (我的平面列表的代码在下面没有样式)

import React, { useState, useContext, useEffect } from "react";
import {
    View,
    Text,
    StyleSheet,
    StatusBar,
    TextInput,
    ScrollView,
    Image,
    ActivityIndicator,
    TouchableOpacity,
    FlatList,
} from "react-native";
import { FirebaseContext } from "../context/FirebaseContext";
import { UserContext } from "../context/UserContext";
import { FontAwesome5, Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import _ from "lodash";
import "firebase/firestore";
import firebase from "firebase";
import config from "../config/firebase";

const SearchScreen = ({ navigation }) => {
    const [searchText, setSearchText] = useState("");
    const [loading, setLoading] = useState(true);
    const [data, setData] = useState([]);
    const [refreshing, setRefreshing] = useState(false);
    const [query, setQuery] = useState("");
    const [userNumLoad, setUserNumLoad] = useState(20);
    const [error, setError] = useState("");

    useEffect(() => {
        const func = async () => {
            await makeRemoteRequest();
        };
        func();
    }, []);

    const contains = (user, query) => {
        if (user.username.includes(query)) {
            return true;
        }
        return false;
    };

    const getUsers = async (limit = 20, query2 = "") => {
        var list = [];
        await firebase
            .firestore()
            .collection("users")
            .get()
            .then((querySnapshot) => {
                querySnapshot.forEach((doc) => {
                    if (doc.data().username.includes(query2)) {
                        list.push({
                            profilePhotoUrl: doc.data().profilePhotoUrl,
                            username: doc.data().username,
                            friends: doc.data().friends.length,
                            uid: doc.data().uid,
                        });
                    }
                });
            });

        setTimeout(() => {
            setData(list);
        }, 4000);


        return new Promise(async (res, rej) => {
            if (query.length === 0) {
                setTimeout(() => {
                    res(_.take(data, limit));
                }, 8000);

            } else {
                const formattedQuery = query.toLowerCase();
                const results = _.filter(data, (user) => {
                    return contains(user, formattedQuery);
                });
                setTimeout(() => {
                    res(_.take(results, limit));
                }, 8000);

            }
        });
    };

    const makeRemoteRequest = _.debounce(async () => {
        const users = [];
        setLoading(true);
        await getUsers(userNumLoad, query)
            .then((users) => {
                setLoading(false);
                setData(users);
                setRefreshing(false);
            })
            .catch((err) => {
                setRefreshing(false);
                setError(err);
                setLoading(false);
                //alert("An error has occured. Please try again later.");
                console.log(err);
            });
    }, 250);

    const handleSearch = async (text) => {
        setSearchText(text);
        const formatQuery = text.toLowerCase();
        await setQuery(text.toLowerCase());
        const data2 = _.filter(data, (user) => {
            return contains(user, formatQuery);
        });
        setData(data2);
        await makeRemoteRequest();
    };

    const handleRefresh = async () => {
        setRefreshing(true);
        await makeRemoteRequest();
    };

    const handleLoadMore = async () => {
        setUserNumLoad(userNumLoad + 20);
        await makeRemoteRequest();
    };

    const renderFooter = () => {
        if (!loading) return null;

        return (
            <View style={{ paddingVertical: 20 }}>
                <ActivityIndicator animating size="large" />
            </View>
        );
    };

    return (
        <View style={styles.container}>
            <View style={styles.header}>
                <TouchableOpacity
                    style={styles.goBackButton}
                    onPress={() => navigation.goBack()}
                >
                    <LinearGradient
                        colors={["#FF5151", "#ac46de"]}
                        style={styles.backButtonGradient}
                    >
                        <Ionicons name="arrow-back" size={30} color="white" />
                    </LinearGradient>
                </TouchableOpacity>
                <View style={styles.spacer} />
                <Text style={styles.headerText}>Search</Text>
                <View style={styles.spacer} />
                <View style={{ width: 46, marginLeft: 15 }}></View>
            </View>
            <View style={styles.inputView}>
                <FontAwesome5 name="search" size={25} color="#FF5151" />
                <TextInput
                    style={styles.input}
                    label="Search"
                    value={searchText}
                    onChangeText={(newSearchText) => handleSearch(newSearchText)}
                    placeholder="Search for people"
                    autoCapitalize="none"
                    autoCorrect={false}
                />
            </View>

            <FlatList
                style={styles.list}
                data={data}
                renderItem={({ item }) => (
                    <TouchableOpacity>
                        <View style={styles.listItem}>
                            <Image
                                style={styles.profilePhoto}
                                source={
                                    item.profilePhotoUrl === "default"
                                        ? require("../../assets/defaultProfilePhoto.jpg")
                                        : { uri: item.profilePhotoUrl }
                                }
                            />
                            <View style={styles.textBody}>
                                <Text style={styles.username}>{item.username}</Text>
                                <Text style={styles.subText}>{item.friends} Friends</Text>
                            </View>
                        </View>
                    </TouchableOpacity>
                )}
                ListFooterComponent={renderFooter}
                keyExtractor={(item) => item.username}
                refreshing={refreshing}
                onEndReachedThreshold={100}
                onEndReached={handleLoadMore}
                onRefresh={handleRefresh}
            />
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1
    },
    searchbar: {
        backgroundColor: 'white'
    },
    header: {
        height: 70,
        flexDirection: 'row',
        justifyContent: 'space-between',
        marginTop: 60,
        paddingLeft: 10,
        paddingRight: 10
    },
    goBackButton: {
        width: 46,
        height: 46,
        borderRadius: 23,
        marginBottom: 10,
        marginLeft: 15
    },
    backButtonGradient: {
        borderRadius: 23,
        height: 46,
        width: 46,
        justifyContent: 'center',
        alignItems: 'center'
    },
    settingsButton: {
        width: 46,
        height: 46,
        borderRadius: 23,
        marginRight: 15,
        marginBottom: 10
    },
    settingsButtonGradient: {
        borderRadius: 23,
        height: 46,
        width: 46,
        justifyContent: 'center',
        alignItems: 'center'
    },
    input: {
        height: 45,
        width: 250,
        paddingLeft: 10,
        fontFamily: "Avenir",
        fontSize: 18
    },
    inputView: {
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
        borderRadius: 50,
        paddingLeft: 10,
        paddingRight: 20,
        shadowColor: 'gray',
        shadowOffset: {width: 5, height: 8},
        shadowOpacity: 0.1,
        backgroundColor: "#ffffff",
        marginRight: 28,
        marginLeft: 28,
        marginTop: 10,
        marginBottom: 25
    },
    headerText: {
        fontSize: 35,
        fontWeight: "800",
        fontFamily: "Avenir",
        color: "#FF5151",
    },
    spacer: {
        width: 50
    },
    listItem: {
        flexDirection: 'row',
        paddingLeft: 15,
        paddingRight: 15,
        paddingTop: 10,
        paddingBottom: 10,
        backgroundColor: "white",
        marginLeft: 20,
        marginRight: 20,
        marginBottom: 10,
        borderRadius: 15,
        alignItems: 'center',
        shadowOpacity: 0.05,
        shadowRadius: 2,
        shadowOffset: {width: 3, height: 3}
    },
    line: {
        width: 100,
        color: 'black',
        height: 1
    },
    profilePhoto: {
        height: 50,
        width: 50,
        borderRadius: 25
    },
    username: {
        fontSize: 18,
        fontFamily: "Avenir",
        paddingBottom: 3
    },
    subText: {
        fontSize: 15,
        fontFamily: "Avenir"
    },
    textBody: {
        flex: 1,
        justifyContent: 'center',
        marginLeft: 20
    }
});

export default SearchScreen;

Here are some observations about your current code.以下是对您当前代码的一些观察。

Wasteful querying of /users /users的浪费查询

you query all user documents in the collection /users regardless of whether you need them or not.无论您是否需要它们,您都可以查询集合/users中的所有用户文档。 For a small number of users, this is fine.对于少数用户来说,这很好。 But as you scale your application to hundreds if not thousands of users, this will quickly become an expensive endeavour.但是,当您将应用程序扩展到数百甚至数千用户时,这将很快成为一项昂贵的工作。

Instead of reading complete documents to only check a username, it is advantageous to query only the data you need.与其阅读完整的文档来只检查用户名,不如只查询您需要的数据。 A more efficient way to achieve this over using Firstore is to create a username index in the Realtime Database (projects can use both the RTDB and Firestore at the same time).比使用 Firstore 更有效的方法是在实时数据库中创建用户名索引(项目可以同时使用 RTDB 和 Firestore)。

Let's say you had the following index:假设您有以下索引:

{
    "usernames": {
        "comegrabfood": "NbTmTrMBN3by4LffctDb03K1sXA2",
        "omegakappas": "zpYzyxSriOMbv4MtlMVn5pUbRaD2",
        "somegal": "SLSjzMLBkBRaccXIhwDOn6nhSqk2",
        "thatguy": "by6fl3R2pCPITXPz8L2tI3IzW223",
        ...
    }
}

which you can build from your users collection (with appropriate permissions and a small enough list of users) using the one off command:您可以使用一次性命令从您的用户集合(具有适当的权限和足够小的用户列表)构建它:

// don't code this in your app, just run from it in a browser window while logged in
// once set up, maintain it while creating/updating usernames
const usersFSRef = firebase.firestore().collection("users");
const usernamesRTRef = firebase.database().ref("usernames");

const usernameUIDMap = {};

usersFSRef.get().then((querySnapshot) => {
    querySnapshot.forEach((userDoc) => {
        usernameUIDMap[userDoc.get("username")] = userDoc.get("uid");
    });
});

usernamesRTRef.set(usernameUIDMap)
  .then(
    () => console.log("Index created successfully"),
    (err) => console.error("Failed to create index")
  );

When no search text is provided, the FlatList should contain the first 20 usernames sorted lexicographically.当没有提供搜索文本时,FlatList 应该包含按字典顺序排序的前 20 个用户名。 For the index above, this would give "comegrabfood" , "omegakappas" , "somegal" , "thatguy" , and so on in that order.对于上面的索引,这将按顺序给出"comegrabfood""omegakappas""somegal""thatguy"等等。 When a user searches usernames containing the text "ome" , we want the username "omegakappas" to appear first in the FlatList because it starts with the search string, but we want "comegrabfood" and "somegal" in the results too.当用户搜索包含文本"ome"的用户名时,我们希望用户名"omegakappas"首先出现在 FlatList 中,因为它以搜索字符串开头,但我们也希望结果中出现"comegrabfood""somegal" If there were at least 20 names that begin with "ome", they should be what appear in the FlatList rather than entries that don't start with the search string.如果至少有 20 个名称以“ome”开头,则它们应该出现在 FlatList 中,而不是不以搜索字符串开头的条目。

Based on that, we have the following requirements:基于此,我们有以下要求:

  • If no search string is provided, return the user data corresponding to the first usernames up to the given limit.如果未提供搜索字符串,则返回与第一个用户名对应的用户数据,直至给定限制。
  • If a search string is provided, return as many entries that start with that string, up to the given limit, if there are remaining slots, find entries that contain "ome" anywhere in the string.如果提供了搜索字符串,则返回以该字符串开头的尽可能多的条目,直到给定的限制,如果有剩余的插槽,则在字符串中的任何位置查找包含"ome"的条目。

The code form of this is:其代码形式为:

// above "const SearchScreen = ..."
const searchUsernames = async (limit = 20, containsString = "") => {
    const usernamesRTRef = firebase.database().ref("usernames");
    const usernameIdPairs = [];

    // if not filtering by a string, just pull the first X usernames sorted lexicographically.
    if (!containsString) {
        const unfilteredUsernameMapSnapshot = await usernamesRTRef.limitToFirst(limit).once('value');
        
        unfilteredUsernameMapSnapshot.forEach((entrySnapshot) => {
            const username = entrySnapshot.key;
            const uid = entrySnapshot.val();
            usernameIdPairs.push({ username, uid });
        });

        return usernameIdPairs;
    }

    // filtering by string, prioritize usernames that start with that string
    const priorityUsernames = {}; // "username" -> true (for deduplication)
    const lowerContainsString = containsString.toLowerCase();
    
    const priorityUsernameMapSnapshot = await usernamesRTRef
        .startAt(lowerContainsString)
        .endAt(lowerContainsString + "/uf8ff")
        .limitToFirst(limit) // only get the first X matching usernames
        .once('value');

    if (priorityUsernameMapSnapshot.hasChildren()) {
        priorityUsernameMapSnapshot.forEach((usernameEntry) => {
            const username = usernameEntry.key;
            const uid = usernameEntry.val();

            priorityUsernames[username] = true;
            usernameIdPairs.push({ username, uid });
        });
    }

    // find out how many more entries are needed
    let remainingCount = limit - usernameIdPairs.length;       

    // set the page size to search
    //   - a small page size will be slow
    //   - a large page size will be wasteful
    const pageSize = 200;

    let lastUsernameOnPage = "";
    while (remainingCount > 0) {
        // fetch up to "pageSize" usernames to scan
        let pageQuery = usernamesRTRef.limitToFirst(pageSize);
        if (lastUsernameOnPage !== "") {
            pageQuery = pageQuery.startAfter(lastUsernameOnPage);
        }
        const fallbackUsernameMapSnapshot = await pageQuery.once('value');

        // no data? break while loop
        if (!fallbackUsernameMapSnapshot.hasChildren()) {
            break;
        }

        // for each username that contains the search string, that wasn't found
        // already above:
        //  - add it to the results array
        //  - decrease the "remainingCount" counter, and if no more results
        //    are needed, break the forEach loop (by returning true)
        fallbackUsernameMapSnapshot.forEach((entrySnapshot) => {
            const username = lastUsernameOnPage = entrySnapshot.key;
            if (username.includes(containsString) && !priorityUsernames[username]) {
                const uid = entrySnapshot.val();
                usernameIdPairs.push({ username, uid });
                // decrease counter and if no entries remain, stop the forEach loop
                return --remainingCount <= 0;
            }
        });
    }

    // return the array of pairs, which will have UP TO "limit" entries in the array
    return usernameIdPairs;
}

Now that we have a list of username-user ID pairs, we need the rest of their user data, which can be fetched using:现在我们有了用户名-用户 ID 对的列表,我们需要用户数据的 rest,可以使用以下方法获取:

// above "const SearchScreen = ..." but below "searchUsernames"
const getUsers = async (limit = 20, containsString = "") => {
    const usernameIdPairs = await searchUsernames(limit, containsString);        

    // compile a list of user IDs, in batches of 10.
    let currentChunk = [], currentChunkLength = 0;
    const chunkedUIDList = [currentChunk];
    for (const pair of usernameIdPairs) {
        if (currentChunkLength === 10) {
            currentChunk = [pair.uid];
            currentChunkLength = 1;
            chunkedUIDList.push(currentChunk);
        } else {
            currentChunk.push(pair.uid);
            currentChunkLength++;
        }
    }

    const uidToDataMap = {}; // uid -> user data
    
    const usersFSRef = firebase.firestore().collection("users");
    
    // fetch each batch of users, adding their data to uidToDataMap
    await Promise.all(chunkedUIDList.map((thisUIDChunk) => (
        usersFSRef
            .where("uid", "in", thisUIDChunk)
            .get()
            .then((querySnapshot) => {
                querySnapshot.forEach(userDataSnapshot => {
                    const uid = userDataSnapshot.id;
                    const docData = userDataSnapshot.data();
                    uidToDataMap[uid] = {
                        profilePhotoUrl: docData.profilePhotoUrl,
                        username: docData.username,
                        friends: docData.friends.length, // consider using friendCount instead
                        uid
                    }
                })
            })
    )));

    // after downloading any found user data, return array of user data,
    // in the same order as usernameIdPairs.
    return usernameIdPairs
        .map(({uid}) => uidToDataMap[uid] || null);
}

Note: While the above code functions, it's still inefficient.注意:虽然上面的代码起作用,但它仍然效率低下。 You could improve performance here by using some third-party text search solution and/or hosting this search in a Callable Cloud Function .您可以通过使用一些第三方文本搜索解决方案和/或在Callable Cloud Function中托管此搜索来提高性能。

Incorrect use of _.debounce _.debounce使用不正确

In your code, when you call handleSearch as you type, the instruction setSearchText is called, which triggers a render of your component.在您的代码中,当您在键入时调用handleSearch时,会调用指令setSearchText ,这会触发您的组件的渲染。 This render then removes all of your functions, the debounced function, getUsers and so on and then recreates them.然后,此渲染会删除您的所有函数,去抖动的 function、 getUsers等,然后重新创建它们。 You need to make sure that when you call one of these state-modifying functions that you are prepared for a redraw.您需要确保当您调用这些状态修改函数之一时,您已准备好进行重绘。 Instead of debouncing makeRemoteRequest , it would be better to debounce the handleSearch function.与其去抖动makeRemoteRequest ,不如去抖动handleSearch function。

const handleSearch = _.debounce(async (text) => {
  setSearchText(text);
  // ...
}, 250);

Sub-optimal use of useEffect useEffect的次优使用

In your code, you make use of useEffect to call makeRemoteRequest() , while this works, you could use useEffect to make the call itself.在您的代码中,您使用useEffect调用makeRemoteRequest() ,虽然这有效,但您可以使用useEffect自己进行调用。 You can then remove all references to makeRemoteRequest() and use the triggered render to make the call.然后,您可以删除对makeRemoteRequest()的所有引用并使用触发的渲染进行调用。

const SearchScreen = ({ navigation }) => {
  const [searchText, setSearchText] = useState(""); // casing searchText to lowercase is handled by `getUsers` and `searchUsernames`, no need for two state variables for the same data
  const [data, setData] = useState([]);
  const [expanding, setExpanding] = useState(true); // shows/hides footer in FlatList (renamed from "loading")
  const [refreshing, setRefreshing] = useState(false); // shows/hides refresh over FlatList
  const [userNumLoad, setUserNumLoad] = useState(20);
  const [error, setError] = useState(""); // note: error is unused in your code at this point

  // decides whether a database call is needed
  // combined so that individual changes of true to false and vice versa
  // for refreshing and expanding don't trigger unnecessary rerenders
  const needNewData = refreshing || expanding; 

  useEffect(() => {
    // if no data is needed, do nothing
    if (!needNewData) return;

    let disposed = false;

    getUsers(userNumLoad, searchText).then(
      (userData) => {
        if (disposed) return; // do nothing if disposed/response out of date
        // these fire a render
        setData(userData);
        setError("");
        setExpanding(false);
        setRefreshing(false);
      },
      (err) => {
        if (disposed) return; // do nothing if disposed/response out of date
        // these fire a render
        setData([]);
        setError(err);
        setExpanding(false);
        setRefreshing(false);
        //alert("An error has occurred. Please try again later.");
        console.log(err);
      }
    );

    return () => disposed = true;
  }, [userNumLoad, searchText, needNewData]); // only rerun this effect when userNumLoad, searchText and needNewData change

  const handleSearch = _.debounce((text) => {
    setSearchText(text); // update query text
    setData([]); // empty existing data
    setExpanding(true); // make database call on next draw
  }, 250);

  const handleRefresh = async () => {
    setRefreshing(true); // make database call on next draw
  };

  const handleLoadMore = async () => {
    setUserNumLoad(userNumLoad + 20); // update query size
    setExpanding(true); // make database call on next draw
  };

  const renderFooter = () => {
    if (!expanding) return null;

    return (
      <View style={{ paddingVertical: 20 }}>
        <ActivityIndicator animating size="large" />
      </View>
    );
  };

  return ( /** your render code here */ );
}

Could you log your users variable in getUsers's then callback?您可以在 getUsers 的 then 回调中记录您的 users 变量吗?

Also, check your FlatList component's style object (styles.list).此外,检查您的 FlatList 组件的样式 object (styles.list)。 It is missing in StyleSheet!样式表中缺少它!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM