React Native Flatlist 搜索不从 firebase 用户集合中返回任何值
React Native Flatlist search returning no values from firebase users collection
所以我最近尝试在 firebase 中对用户进行 FlatList 搜索,但我 运行 遇到了一堆错误,尽管代码中似乎没有错误。目前,该列表搜索并没有 return 任何东西,尽管“用户”的 firebase 集合中显然有数据。当我尝试在 getUsers() 中的 Promise 的解析语句上方记录“结果”时,我突然看到用户,尽管我收到“结果”不存在的错误,这很奇怪,因为为什么会出现错误让代码工作?无论如何,如果有人能够帮助我尝试使这个 FlatList 工作,我将不胜感激。我已经为此工作了 3 天,似乎无法在线找到任何解决方案或修复代码。为了你的帮助,我很乐意给你一个邓肯甜甜圈,因为这对我来说意义重大。感谢所有帮助和提示,并提前感谢您的宝贵时间! (下面是我的平面列表的代码,没有样式)
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;
你能在 getUsers 的 then 回调中记录你的 users 变量吗?
此外,请检查 FlatList 组件的样式对象 (styles.list)。样式表中缺少它!
以下是对您当前代码的一些观察。
/users
的浪费查询
您查询集合 /users
中的所有用户文档,无论您是否需要它们。对于少数用户来说,这很好。但是,当您将应用程序扩展到数百甚至数千个用户时,这将很快成为一项昂贵的工作。
与其阅读完整的文档来只检查用户名,不如只查询您需要的数据。比使用 Firstore 更有效的方法是在实时数据库中创建用户名索引(项目可以同时使用 RTDB 和 Firestore)。
假设您有以下索引:
{
"usernames": {
"comegrabfood": "NbTmTrMBN3by4LffctDb03K1sXA2",
"omegakappas": "zpYzyxSriOMbv4MtlMVn5pUbRaD2",
"somegal": "SLSjzMLBkBRaccXIhwDOn6nhSqk2",
"thatguy": "by6fl3R2pCPITXPz8L2tI3IzW223",
...
}
}
您可以使用一次性命令从您的用户集合中构建(具有适当的权限和足够小的用户列表):
// 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")
);
如果未提供搜索文本,FlatList 应包含按字典顺序排序的前 20 个用户名。对于上面的索引,这将按顺序给出 "comegrabfood"
、"omegakappas"
、"somegal"
、"thatguy"
等。当用户搜索包含文本 "ome"
的用户名时,我们希望用户名 "omegakappas"
在 FlatList 中首先出现,因为它以搜索字符串开头,但我们希望 "comegrabfood"
和 "somegal"
在结果中。如果至少有 20 个名称以“ome”开头,则它们应该出现在 FlatList 中,而不是不以搜索字符串开头的条目。
基于此,我们有以下要求:
- 如果未提供搜索字符串,return 与达到给定限制的第一个用户名对应的用户数据。
- 如果提供了搜索字符串,return 以该字符串开头的条目数量不超过给定的限制,如果还有剩余槽位,则在搜索字符串中的任意位置查找包含
"ome"
的条目字符串。
其代码形式为:
// 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;
}
现在我们有了用户名-用户 ID 对列表,我们需要他们的其余用户数据,可以使用以下方法获取:
// 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);
}
注意:虽然上面的代码起作用了,但效率仍然很低。您可以通过使用一些第三方文本搜索解决方案 and/or 在 Callable Cloud Function.
中托管此搜索来提高性能
错误使用_.debounce
在您的代码中,当您在键入时调用 handleSearch
时,会调用指令 setSearchText
,这会触发组件的渲染。此渲染然后删除所有函数、去抖动函数、getUsers
等等,然后重新创建它们。您需要确保在调用这些状态修改函数之一时已准备好重绘。与其去抖makeRemoteRequest
,不如去handleSearch
函数去抖
const handleSearch = _.debounce(async (text) => {
setSearchText(text);
// ...
}, 250);
useEffect
的次优使用
在您的代码中,您使用 useEffect
来调用 makeRemoteRequest()
,虽然这有效,但您可以使用 useEffect
自己进行调用。然后,您可以删除对 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 */ );
}
所以我最近尝试在 firebase 中对用户进行 FlatList 搜索,但我 运行 遇到了一堆错误,尽管代码中似乎没有错误。目前,该列表搜索并没有 return 任何东西,尽管“用户”的 firebase 集合中显然有数据。当我尝试在 getUsers() 中的 Promise 的解析语句上方记录“结果”时,我突然看到用户,尽管我收到“结果”不存在的错误,这很奇怪,因为为什么会出现错误让代码工作?无论如何,如果有人能够帮助我尝试使这个 FlatList 工作,我将不胜感激。我已经为此工作了 3 天,似乎无法在线找到任何解决方案或修复代码。为了你的帮助,我很乐意给你一个邓肯甜甜圈,因为这对我来说意义重大。感谢所有帮助和提示,并提前感谢您的宝贵时间! (下面是我的平面列表的代码,没有样式)
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;
你能在 getUsers 的 then 回调中记录你的 users 变量吗?
此外,请检查 FlatList 组件的样式对象 (styles.list)。样式表中缺少它!
以下是对您当前代码的一些观察。
/users
的浪费查询
您查询集合 /users
中的所有用户文档,无论您是否需要它们。对于少数用户来说,这很好。但是,当您将应用程序扩展到数百甚至数千个用户时,这将很快成为一项昂贵的工作。
与其阅读完整的文档来只检查用户名,不如只查询您需要的数据。比使用 Firstore 更有效的方法是在实时数据库中创建用户名索引(项目可以同时使用 RTDB 和 Firestore)。
假设您有以下索引:
{
"usernames": {
"comegrabfood": "NbTmTrMBN3by4LffctDb03K1sXA2",
"omegakappas": "zpYzyxSriOMbv4MtlMVn5pUbRaD2",
"somegal": "SLSjzMLBkBRaccXIhwDOn6nhSqk2",
"thatguy": "by6fl3R2pCPITXPz8L2tI3IzW223",
...
}
}
您可以使用一次性命令从您的用户集合中构建(具有适当的权限和足够小的用户列表):
// 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")
);
如果未提供搜索文本,FlatList 应包含按字典顺序排序的前 20 个用户名。对于上面的索引,这将按顺序给出 "comegrabfood"
、"omegakappas"
、"somegal"
、"thatguy"
等。当用户搜索包含文本 "ome"
的用户名时,我们希望用户名 "omegakappas"
在 FlatList 中首先出现,因为它以搜索字符串开头,但我们希望 "comegrabfood"
和 "somegal"
在结果中。如果至少有 20 个名称以“ome”开头,则它们应该出现在 FlatList 中,而不是不以搜索字符串开头的条目。
基于此,我们有以下要求:
- 如果未提供搜索字符串,return 与达到给定限制的第一个用户名对应的用户数据。
- 如果提供了搜索字符串,return 以该字符串开头的条目数量不超过给定的限制,如果还有剩余槽位,则在搜索字符串中的任意位置查找包含
"ome"
的条目字符串。
其代码形式为:
// 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;
}
现在我们有了用户名-用户 ID 对列表,我们需要他们的其余用户数据,可以使用以下方法获取:
// 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);
}
注意:虽然上面的代码起作用了,但效率仍然很低。您可以通过使用一些第三方文本搜索解决方案 and/or 在 Callable Cloud Function.
中托管此搜索来提高性能错误使用_.debounce
在您的代码中,当您在键入时调用 handleSearch
时,会调用指令 setSearchText
,这会触发组件的渲染。此渲染然后删除所有函数、去抖动函数、getUsers
等等,然后重新创建它们。您需要确保在调用这些状态修改函数之一时已准备好重绘。与其去抖makeRemoteRequest
,不如去handleSearch
函数去抖
const handleSearch = _.debounce(async (text) => {
setSearchText(text);
// ...
}, 250);
useEffect
的次优使用
在您的代码中,您使用 useEffect
来调用 makeRemoteRequest()
,虽然这有效,但您可以使用 useEffect
自己进行调用。然后,您可以删除对 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 */ );
}