React+Formik+Firestore开发Ins克隆版遇权限不足及空值报错
两类异常的根因与修复方案
错误根因汇总
1. 快照监听权限不足报错(Missing or insufficient permissions)
- 集合名拼写错误:查询用户信息时调用的是
db.collection('user')(单数命名),但Firestore安全规则仅配置了复数路径/users/{userId}的访问权限,未匹配到规则的请求会被Firestore默认拒绝。 - 查询条件传值错误:
where('owner_uid', '==', 'user.uid')把变量user.uid写成了固定字符串,即使集合名正确也无法查询到目标用户文档。 onSnapshot未绑定错误处理回调,监听抛出的异常直接冒泡到全局。
2. 提交表单时空对象报错(null is not an object)
- 状态初始值问题:
currentLoggedInUser初始值为null,用户信息未完成加载时点击提交,访问currentLoggedInUser.username会直接触发空指针错误。 - 前置查询失败:由于上述用户信息查询的拼写、传值错误,用户数据从未成功拉取,
currentLoggedInUser始终为null,提交必然报错。 - 副作用未清理:
useEffect中注册的快照监听没有在组件卸载时取消,会造成内存泄漏。 - 上传逻辑未做空值校验,也未捕获Promise异常,错误直接触发Formik的未处理错误警告。
分步修复方案
第一步:修正用户信息查询逻辑
- 统一集合名为
users,和安全规则、上传逻辑的命名保持一致 - 修正where条件的传值,使用实际的用户UID变量而非字符串
- 给快照监听添加错误回调,在useEffect返回值中调用unsubscribe清理监听
- 新增加载态,用户信息未加载完成时禁用提交按钮
第二步:修正表单提交逻辑
- 提交前校验
currentLoggedInUser是否已赋值,未加载完成直接终止提交 - 合并图片输入框的重复事件绑定,同时更新缩略图预览和表单值
- 给上传Promise添加catch捕获异常,避免未处理错误冒泡
- 确保上传成功后再触发导航返回
第三步:确认Firestore安全规则配置
现有规则已覆盖根级users集合和所有子级posts集合的认证访问权限,只要修正代码中的集合名拼写即可正常访问,无需额外调整规则。基础可用的规则参考如下:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function userIsAuthenticated() { return request.auth != null; } // 匹配所有层级下的posts集合 match /{path=**}/posts/{postId} { allow read, write: if userIsAuthenticated(); } // 匹配根级用户集合 match /users/{userId} { allow read, write: if userIsAuthenticated(); } } }
修正后的完整业务代码
import { View, Text, Image, TextInput, Button, ActivityIndicator } from 'react-native' import React, { useState, useEffect } from 'react' import * as Yup from 'yup' import { Formik } from 'formik' import { Divider } from 'react-native-elements' import validUrl from 'valid-url' import {db, firebase} from '../../firebase' const PLACEHOLDER_IMG = 'https://pacificpatiostructures.com/wp-content/uploads/2016/06/import_placeholder.png' const uploadPostSchema = Yup.object().shape({ imageUrl: Yup.string().url().required('A URL is required'), caption: Yup.string().max(2200, 'Caption has reached the character limit.') }) const FormikPostUploader = ({ navigation }) => { const [thumbnailUrl, setThumbnailUrl] = useState(PLACEHOLDER_IMG) const [currentLoggedInUser, setCurrentLoggedInUser] = useState(null) const [loadingUser, setLoadingUser] = useState(true) const getUsername = () => { const user = firebase.auth().currentUser // 修正集合名拼写、where条件传值错误 const unsubscribe = db .collection('users') .where('owner_uid', '==', user.uid).limit(1).onSnapshot( snapshot => { snapshot.docs.map(doc => { setCurrentLoggedInUser({ username: doc.data().username, profilePicture: doc.data().profile_picture, }) setLoadingUser(false) }) }, // 新增监听错误回调 (error) => { console.log('用户信息监听失败:', error) setLoadingUser(false) } ) return unsubscribe } useEffect(() => { const unsubscribe = getUsername() // 组件卸载时取消监听,避免内存泄漏 return () => unsubscribe() }, []) const uploadPostToFirebase = async (imageUrl, caption) => { // 新增空值校验,用户信息未加载完成直接终止提交 if (!currentLoggedInUser || !firebase.auth().currentUser) return try { await db .collection('users') .doc(firebase.auth().currentUser.email).collection('posts') .add({ imageUrl: imageUrl, user: currentLoggedInUser.username, profile_picture: currentLoggedInUser.profilePicture, owner_uid: firebase.auth().currentUser.uid, caption: caption, createdAt: firebase.firestore.FieldValue.serverTimestamp(), likes: 0, likes_by_users: [], comments: [], }) navigation.goBack() } catch (error) { console.log('帖子上传失败:', error) } } return ( <Formik initialValues={{caption: '', imageUrl: ''}} onSubmit={values => { uploadPostToFirebase(values.imageUrl, values.caption) }} validationSchema={uploadPostSchema} validateOnMount={true} > {({ handleBlur, handleChange, handleSubmit, values, errors, isValid }) => ( <> <View style={{ margin: 20, justifyContent: 'space-between', flexDirection: 'row', }}> <Image source={{ uri: validUrl.isUri(thumbnailUrl) ? thumbnailUrl : PLACEHOLDER_IMG}} style={{ width: 100, height: 100 }}/> <View style={{ flex: 1, marginLeft: 12 }}> <TextInput style={{ color: 'white', fontSize: 20 }} placeholder='Write a caption...' placeholderTextColor='gray' multiline={true} onChangeText={handleChange('caption')} onBlur={handleBlur('caption')} value={values.caption} /> </View> </View> <Divider width = {0.2} orientation='vertical' /> <TextInput style={{ color: 'white', fontSize: 18 }} placeholder='Enter Image Url' placeholderTextColor='gray' // 合并重复事件绑定,同时更新缩略图和表单值 onChangeText={(text) => { setThumbnailUrl(text) handleChange('imageUrl')(text) }} onBlur={handleBlur('imageUrl')} value={values.imageUrl} /> {errors.imageUrl &&( <Text style={{ fontSize: 10, color: 'red' }}> {errors.imageUrl} </Text> )} {/* 加载中或表单无效时禁用按钮 */} <Button onPress={handleSubmit} title={loadingUser ? '加载中...' : 'Share'} disabled={!isValid || loadingUser} /> </> )} </Formik> ) } export default FormikPostUploader
内容的提问来源于stack exchange,提问作者R3CURSION
相关产品推荐
相关产品推荐

