You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native+Firebase:如何在Firestore查询返回后渲染视图?

React Native Firestore异步数据渲染问题解决实践

问题描述

我是React Native新手,希望在Firestore查询返回用户对象后再渲染视图。当前代码中使用普通变量USER存储查询结果,但因异步处理不当,视图渲染时USER为null,尽管在snapshot.docs.forEach()中能打印到目标对象,现寻求该问题的最佳解决实践。

原代码

import React, {useEffect, useState} from 'react';
import { View, Text, Image, ScrollView, TouchableOpacity } from 'react-native';
import { FIREBASE_AUTH, FIREBASE_DB } from '../firebase/config'
import { collection, doc, getDoc, where,query, onSnapshot } from 'firebase/firestore'
const ProfileView = () => {      
  var USER = null;   
  useEffect(() => {
    let isMounted = true;
    var auth = FIREBASE_AUTH
    var db = FIREBASE_DB
    var user = auth.currentUser
    
    const doFetch = async () => {
      const dbRef = collection(db, 'Users');    
      const q = query(dbRef, where("uid", "==", user.uid))
      onSnapshot(q, (snapshot) => {   
        snapshot.docs.forEach((doc) => {
            USER = doc.data();
        })
    })
    };  
    doFetch()
      .then(() => {
        return (
          <ScrollView style={styles.container}>
              <View style={styles.headerContainer}>
                  <Image
                      style={styles.coverPhoto}
                      source={{uri: 'https://www.bootdey.com/image/250x250/9400D3/9400D3'}}
                  />
                  <View style={styles.profileContainer}>
                      <Image
                      style={styles.profilePhoto}
                      source={{uri: 'https://www.bootdey.com/img/Content/avatar/avatar1.png'}}
                      />
                      <Text style={styles.nameText}>{USER.firstname} {USER.lastname}</Text>
                  </View>
              </View>
    
              <View style={styles.section}>
                  <Text style={styles.statCount}>1234</Text>
                  <Text style={styles.statLabel}>Friends</Text>
              </View>
    
              <View style={styles.section}>
                  <View>
                      <ScrollView horizontal contentContainerStyle={styles.friendsScroll}>                      
                      </ScrollView>
                  </View>
              </View>
    
              <View style={styles.section}>
                  <Text style={styles.bioText}>
                      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed et
                      ullamcorper nisi.
                  </Text>
              </View>
    
              <View style={styles.section}>
                  <TouchableOpacity style={styles.button} onPress={handleEditPress}>
                      <Text style={styles.buttonText}>Edit Profile</Text>
                  </TouchableOpacity>            
              </View>
          </ScrollView>
        );
      })
      .catch((err) => {
        if (!isMounted) return; // unmounted, ignore.
        // TODO: Handle errors in your component
        console.error("failed to fetch data", err);
      });
      
  
    return () => {
      
    };
  }, []);
    
   }   

const styles = {
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  headerContainer: {
    alignItems: 'center',
  },
  coverPhoto: {
    width: '100%',
    height: 180,
  },
  profileContainer: {
    alignItems: 'center',
    marginTop: -70,
  },
  profilePhoto: {
    width: 150,
    height: 150,
    borderRadius: 75,
  },
  nameText: {
    fontSize: 20,
    fontWeight: 'bold',
    marginTop: 10,
  },
  bioContainer: {
    padding: 15,
  },
  bioText: {
    fontSize: 16,
    textAlign:'center',
    color:'#A9A9A9'
  },
  section: {
    flexDirection:'row',
    alignItems: 'center',
    justifyContent: 'center',
    width:'100%',
    marginVertical:5,
    paddingHorizontal:10,
  },
  statCount: {
    color: '#999',
    fontSize: 16,
    fontWeight: 'bold',
  },
  statLabel: {
    fontSize: 16,
    color: '#999',
    marginLeft:4
  },
  button: {
    backgroundColor: '#9400D3',
    borderRadius: 5,
    padding: 10,
    marginHorizontal: 20,
  },
  buttonText: {
    fontSize: 16,
    color: '#fff',
    textAlign: 'center',
  },
  friendCard:{
    width:50,
    height:50,
    borderRadius:25,
    marginLeft:2,
  },
  friendImage:{
    width:50,
    height:50,
    borderRadius:25,
  },
  friendsScroll:{
    paddingBottom:10,

  }
};

export default ProfileView;

解决方案

核心问题分析

原代码存在两个关键错误:

  1. 使用普通变量存储异步数据,无法触发React组件重新渲染
  2. 错误地在useEffect的异步回调中返回JSX,不符合React组件的渲染逻辑

修正步骤

  1. 用useState管理用户状态
    React状态变量更新会自动触发组件重新渲染,替代普通变量:

    const [user, setUser] = useState({});
    const [isLoading, setIsLoading] = useState(true); // 可选:添加加载状态提升用户体验
    
  2. 修正数据获取逻辑
    在Firestore快照回调中更新状态,同时添加组件挂载状态判断和订阅清理,避免内存泄漏:

    useEffect(() => {
      let isMounted = true;
      const auth = FIREBASE_AUTH;
      const db = FIREBASE_DB;
      const currentUser = auth.currentUser;
      
      if (!currentUser) {
        setIsLoading(false);
        return;
      }
    
      const doFetch = () => {
        const dbRef = collection(db, 'Users');    
        const q = query(dbRef, where("uid", "==", currentUser.uid));
        // 保存订阅函数,用于组件卸载时清理
        const unsubscribe = onSnapshot(q, (snapshot) => {   
          snapshot.docs.forEach((doc) => { 
            if(isMounted) {
              setUser(doc.data());
              setIsLoading(false);
            }            
          })
        }, (err) => {
          console.error("获取用户数据失败", err);
          setIsLoading(false);
        });
    
        return unsubscribe;
      };  
    
      const unsubscribe = doFetch();
        
      return () => {
        isMounted = false;
        unsubscribe(); // 取消Firestore快照订阅
      };
    }, []);
    
  3. 修正组件渲染逻辑
    将JSX放在组件函数的返回值中,而非异步回调内,同时处理加载状态:

    // 加载状态显示
    if (isLoading) {
      return (
        <View style={{flex:1, justifyContent:'center', alignItems:'center'}}>
          <Text>加载中...</Text>
        </View>
      );
    }
    
    // 主视图渲染
    return (
      <ScrollView style={styles.container}>
          {/* 省略其他视图代码 */}
          <Text style={styles.nameText}>{user.firstname} {user.lastname}</Text>
          {/* 省略其他视图代码 */}
      </ScrollView>
    );
    

完整修正代码

import React, {useEffect, useState} from 'react';
import { View, Text, Image, ScrollView, TouchableOpacity } from 'react-native';
import { FIREBASE_AUTH, FIREBASE_DB } from '../firebase/config'
import { collection, where, query, onSnapshot } from 'firebase/firestore'

const ProfileView = () => {      
  const [user, setUser] = useState({});   
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    let isMounted = true;
    const auth = FIREBASE_AUTH;
    const db = FIREBASE_DB;
    const currentUser = auth.currentUser;
    
    if (!currentUser) {
      setIsLoading(false);
      return;
    }

    const doFetch = () => {
      const dbRef = collection(db, 'Users');    
      const q = query(dbRef, where("uid", "==", currentUser.uid));
      const unsubscribe = onSnapshot(q, (snapshot) => {   
        snapshot.docs.forEach((doc) => { 
          if(isMounted) {
            setUser(doc.data());
            setIsLoading(false);
          }            
        })
      }, (err) => {
        console.error("获取用户数据失败", err);
        setIsLoading(false);
      });

      return unsubscribe;
    };  

    const unsubscribe = doFetch();
      
    return () => {
      isMounted = false;
      unsubscribe();
    };
  }, []);

  if (isLoading) {
    return (
      <View style={{flex:1, justifyContent:'center', alignItems:'center'}}>
        <Text>加载中...</Text>
      </View>
    );
  }

  return (
    <ScrollView style={styles.container}>
        <View style={styles.headerContainer}>
            <Image
                style={styles.coverPhoto}
                source={{uri: 'https://www.bootdey.com/image/250x250/9400D3/9400D3'}}
            />
            <View style={styles.profileContainer}>
                <Image
                style={styles.profilePhoto}
                source={{uri: 'https://www.bootdey.com/img/Content/avatar/avatar1.png'}}
                />
                <Text style={styles.nameText}>{user.firstname} {user.lastname}</Text>
            </View>
        </View>

        <View style={styles.section}>
            <Text style={styles.statCount}>1234</Text>
            <Text style={styles.statLabel}>好友</Text>
        </View>

        <View style={styles.section}>
            <View>
                <ScrollView horizontal contentContainerStyle={styles.friendsScroll}>                      
                </ScrollView>
            </View>
        </View>

        <View style={styles.section}>
            <Text style={styles.bioText}>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed et
                ullamcorper nisi.
            </Text>
        </View>

        <View style={styles.section}>
            <TouchableOpacity style={styles.button} onPress={() => {}}>
                <Text style={styles.buttonText}>编辑资料</Text>
            </TouchableOpacity>            
        </View>
    </ScrollView>
  );
}   

const styles = {
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  headerContainer: {
    alignItems: 'center',
  },
  coverPhoto: {
    width: '100%',
    height: 180,
  },
  profileContainer: {
    alignItems: 'center',
    marginTop: -70,
  },
  profilePhoto: {
    width: 150,
    height: 150,
    borderRadius: 75,
  },
  nameText: {
    fontSize: 20,
    fontWeight: 'bold',
    marginTop: 10,
  },
  bioContainer: {
    padding: 15,
  },
  bioText: {
    fontSize: 16,
    textAlign:'center',
    color:'#A9A9A9'
  },
  section: {
    flexDirection:'row',
    alignItems: 'center',
    justifyContent: 'center',
    width:'100%',
    marginVertical:5,
    paddingHorizontal:10,
  },
  statCount: {
    color: '#999',
    fontSize: 16,
    fontWeight: 'bold',
  },
  statLabel: {
    fontSize: 16,
    color: '#999',
    marginLeft:4
  },
  button: {
    backgroundColor: '#9400D3',
    borderRadius: 5,
    padding: 10,
    marginHorizontal: 20,
  },
  buttonText: {
    fontSize: 16,
    color: '#fff',
    textAlign: 'center',
  },
  friendCard:{
    width:50,
    height:50,
    borderRadius:25,
    marginLeft:2,
  },
  friendImage:{
    width:50,
    height:50,
    borderRadius:25,
  },
  friendsScroll:{
    paddingBottom:10,

  }
};

export default ProfileView;

内容的提问来源于stack exchange,提问作者RonanD1807

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 12:27:34