React Native中Firebase+Expo Auth Session谷歌认证加载错误问题
问题背景
在使用Firebase和Expo Auth Session实现React Native应用的谷歌认证时,遇到如下错误:
Error: Cannot prompt to authenticate until the request has finished loading.
应用结构与流程
- 核心组件为
App和LoginScreen,均通过promptAsyncGoogle函数触发谷歌认证 App组件中通过Google.useAuthRequest钩子初始化promptAsyncGoogle,用于应用启动时触发认证LoginScreen组件通过route.params获取该函数,点击“Logga in med Google”按钮时调用
错误现象
- 直接点击
LoginScreen的谷歌登录按钮会触发上述错误 - 先点击
CreateAccount组件(同样使用promptAsyncGoogle)的谷歌登录按钮后,LoginScreen的认证功能恢复正常
相关代码
App组件代码
// Code from App.js // ... useEffect(() => { const { request, response, promptAsyncGoogle } = Google.useAuthRequest({ useProxy: true, expoClientId: '', androidClientId: '', }); // ... useEffect(() => { if (response?.type === "success") { // Handle successful authentication } else if (response?.type === "dismiss" || response?.type === "error") { // Handle dismissal or error } }, [response]); // ... }, []);
LoginScreen组件代码
const LoginScreen = ({ route }) => { const { setInfoEntered, promptAsyncGoogle, setLoading, setUser } = route.params //.. <TouchableOpacity style={styles.googleButton} onPress={() => { promptAsyncGoogle()}}> <View style={styles.buttonIcon}> <AntDesign name="google" style={styles.googleIcon} /> </View> <Text style={styles.googleText}>Logga in med Google</Text> </TouchableOpacity>
问题解答
1. LoginScreen组件中出现该错误的原因是什么?
Google.useAuthRequest钩子初始化时,需要先完成认证请求的加载过程(生成有效的请求参数),才能安全调用promptAsyncGoogle。你在App组件的useEffect中初始化该钩子,但没有等待request对象完全就绪,就将promptAsyncGoogle传递给了LoginScreen。此时用户点击按钮调用函数时,请求仍处于未加载完成的状态,因此抛出该错误。
2. 为何先在CreateAccount组件中使用谷歌认证后,LoginScreen的认证才能正常工作?
当你在CreateAccount组件中调用promptAsyncGoogle时,实际触发了请求的加载流程(或者此时request刚好完成加载)。一旦请求加载完成,promptAsyncGoogle就处于就绪状态,后续在LoginScreen中调用同一个函数时,就不会再触发未加载的错误。
3. 如何确保LoginScreen的promptAsyncGoogle函数从一开始就能正常工作,无需先在其他组件中使用?
可以通过以下几种方案解决:
方案1:等待request就绪后再传递函数
在App组件中,使用状态变量存储promptAsyncGoogle,仅当request对象就绪后再更新状态,确保传递给子组件的是已就绪的函数:
// App.js 修改部分 import { useState, useEffect } from 'react'; const App = () => { const [promptAsyncGoogle, setPromptAsyncGoogle] = useState(null); useEffect(() => { const { request, response, promptAsyncGoogle: tempPrompt } = Google.useAuthRequest({ useProxy: true, expoClientId: 'YOUR_CLIENT_ID', androidClientId: 'YOUR_ANDROID_CLIENT_ID', }); // 当request存在时,说明请求已加载完成 if (request) { setPromptAsyncGoogle(tempPrompt); } // 处理response逻辑 useEffect(() => { if (response?.type === "success") { // 处理认证成功逻辑 } else if (response?.type === "dismiss" || response?.type === "error") { // 处理取消或错误逻辑 } }, [response]); }, []); // 导航到LoginScreen时,仅传递已就绪的函数 navigation.navigate('LoginScreen', { setInfoEntered, promptAsyncGoogle, setLoading, setUser }); };
方案2:在LoginScreen中禁用按钮直到函数就绪
在LoginScreen中,检查promptAsyncGoogle是否存在,不存在时禁用登录按钮,避免用户提前点击:
// LoginScreen.js 修改部分 <TouchableOpacity style={[styles.googleButton, !promptAsyncGoogle && styles.disabledButton]} onPress={() => { promptAsyncGoogle()}} disabled={!promptAsyncGoogle} > <View style={styles.buttonIcon}> <AntDesign name="google" style={styles.googleIcon} /> </View> <Text style={styles.googleText}>Logga in med Google</Text> </TouchableOpacity> // 可添加禁用样式 const styles = StyleSheet.create({ // ...其他样式 disabledButton: { opacity: 0.5, } });
方案3:使用React Context全局管理认证逻辑
将谷歌认证的请求和函数抽离到React Context中,让所有需要的组件从Context中获取已就绪的promptAsyncGoogle,避免通过路由参数传递的时机问题:
// 创建AuthContext.js import { createContext, useContext, useState, useEffect } from 'react'; import * as Google from 'expo-auth-session/providers/google'; const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [promptAsyncGoogle, setPromptAsyncGoogle] = useState(null); const [request, response, promptAsync] = Google.useAuthRequest({ useProxy: true, expoClientId: 'YOUR_CLIENT_ID', androidClientId: 'YOUR_ANDROID_CLIENT_ID', }); useEffect(() => { if (request) { setPromptAsyncGoogle(promptAsync); } }, [request]); // 处理response逻辑 useEffect(() => { if (response?.type === "success") { // 处理认证成功逻辑 } else if (response?.type === "dismiss" || response?.type === "error") { // 处理取消或错误逻辑 } }, [response]); return ( <AuthContext.Provider value={{ promptAsyncGoogle }}> {children} </AuthContext.Provider> ); }; export const useAuth = () => useContext(AuthContext);
然后在App.js中包裹组件:
import { AuthProvider } from './AuthContext'; const App = () => { return ( <AuthProvider> {/* 你的导航组件或根组件 */} </AuthProvider> ); };
最后在LoginScreen中使用:
import { useAuth } from './AuthContext'; const LoginScreen = () => { const { promptAsyncGoogle } = useAuth(); // ...其他逻辑 return ( <TouchableOpacity style={[styles.googleButton, !promptAsyncGoogle && styles.disabledButton]} onPress={() => { promptAsyncGoogle()}} disabled={!promptAsyncGoogle} > {/* 按钮内容 */} </TouchableOpacity> ); };
内容的提问来源于stack exchange,提问作者LetItBurn

