如何在React Native中用expo-auth-session和Firebase拆分多登录提供商逻辑
问题描述
在使用Firebase和expo-auth-session实现React Native多登录提供商功能时,希望将Google登录逻辑抽离到单独文件,同时为其他提供商创建对应文件,在LoginScreen.js中调用相关函数。但尝试的写法触发Hook错误:Invalid hook call. Hooks can only be called inside of the body of a function component,需要解决拆分代码的同时正确管理promptAsync()的问题。
可运行的Google登录代码
import React, {useState, useEffect} from "react"; import { View, StyleSheet } from "react-native"; import { AppTitle, Screen, AppLink, SpecialText, AppButtonPrimary, AppText, ActivityIndicator } from "../../components"; import routes from "../../navigation/routes"; import colors from "../../config/colors"; import * as WebBrowser from 'expo-web-browser'; import { signInWithCredential, GoogleAuthProvider } from "firebase/auth"; import { auth } from '../../config/firebase'; import * as Google from 'expo-auth-session/providers/google'; import logger from '../../utility/logger' WebBrowser.maybeCompleteAuthSession(); function LoginScreen({ navigation }) { const [loadingPage, setLoadingPage] = useState(false) const [request, response, promptAsync] = Google.useIdTokenAuthRequest( { clientId: 'xxxxxxxxxx.apps.googleusercontent.com', }, ); useEffect(() => { if (response?.type === 'success') { setLoadingPage(true) const { id_token } = response.params; const credential = GoogleAuthProvider.credential(id_token); logUser(auth, credential); } }, [response]); async function logUser(auth, credential) { try { const result = await signInWithCredential(auth, credential) const user = result.user // User is signed in // Some code } catch(err) { logger.log(err) // Some code } } return ( <Screen style={styles.container}> <ActivityIndicator visible={loadingPage}/> <AppTitle style={styles.title} size={30}>Connectez-vous à votre compte</AppTitle> <View style={styles.buttons}> <AppButtonPrimary text="Email" icon="envelope-o" onPress={() => navigation.navigate(routes.LOGIN_EMAIL_SCREEN)} width={"80%"} backColor={colors.white} textColor={colors.grayDark} /> <AppButtonPrimary text="Google" icon="google" onPress={() => {promptAsync()}} // <=== how to pass a function here instead of this promptAsync ?! width={"80%"} backColor={colors.white} textColor={colors.grayDark} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", }, title: { marginTop: 30, marginHorizontal:10, textAlign: 'center' }, buttons: { width: "100%", alignItems: "center", marginTop: 20, } }); export default LoginScreen;
尝试拆分后的代码
LoginScreen.js
import React, {useState} from "react"; import { View, StyleSheet } from "react-native"; import { AppTitle, Screen, AppLink, SpecialText, AppButtonPrimary, AppText, ActivityIndicator } from "../../components"; import routes from "../../navigation/routes"; import colors from "../../config/colors"; import googleAuth from "../../auth/googleAuth"; import facebookAuth from "../../auth/facebookAuth"; function LoginScreen({ navigation }) { const [loadingPage, setLoadingPage] = useState(false) function googleSignIn() { setLoadingPage(true) googleAuth() } return ( <Screen style={styles.container}> <ActivityIndicator visible={loadingPage}/> <AppTitle style={styles.title} size={30}>Connectez-vous à votre compte</AppTitle> <View style={styles.buttons}> <AppButtonPrimary text="Email" icon="envelope-o" onPress={() => navigation.navigate(routes.LOGIN_EMAIL_SCREEN)} width={"80%"} backColor={colors.white} textColor={colors.grayDark} /> <AppButtonPrimary text="Google" icon="google" onPress={googleSignIn} // <=== here width={"80%"} backColor={colors.white} textColor={colors.grayDark} /> <AppButtonPrimary text="Facebook" icon="facebook" onPress={facebookSignIn} // <=== this is the idea (having multiple providers with their own logic in a separate file) width={"80%"} backColor={colors.white} textColor={colors.grayDark} /> </View> </Screen> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", }, title: { marginTop: 30, marginHorizontal:10, textAlign: 'center' }, buttons: { width: "100%", alignItems: "center", marginTop: 20, } }); export default LoginScreen;
googleAuth.js
// Infrastructure import { useEffect } from "react" import { ResponseType } from 'expo-auth-session'; import * as Google from 'expo-auth-session/providers/google'; import * as WebBrowser from 'expo-web-browser'; // Firebase import { signInWithCredential, GoogleAuthProvider } from "firebase/auth"; import { auth } from '../../app/config/firebase'; WebBrowser.maybeCompleteAuthSession(); function googleAuth() { const [request, response, promptAsync] = Google.useIdTokenAuthRequest( { clientId: 'xxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com', }, ); useEffect(() => { if (response?.type === 'success') { const { id_token } = response.params; const credential = GoogleAuthProvider.credential(id_token); logUser(auth, credential); } }, [response]); async function logUser(auth, credential) { try { const result = await signInWithCredential(auth, credential) const user = result.user // User is signed in // Some Code } catch(err) { // Some Code } } } export default googleAuth
解决方案
核心原因
错误是因为你在普通函数googleAuth()里调用了React Hook(Google.useIdTokenAuthRequest和useEffect),违反了React Hook规则:Hook只能在函数组件的顶层或自定义Hook的顶层调用。
解决思路
将每个登录提供商的逻辑封装为自定义Hook,自定义Hook可以合法调用其他Hook,同时返回供组件使用的登录触发函数,以及通过回调处理状态和结果。
修改后的代码
1. 封装Google登录为自定义Hook(googleAuth.js)
// Infrastructure import { useEffect } from "react" import * as Google from 'expo-auth-session/providers/google'; import * as WebBrowser from 'expo-web-browser'; // Firebase import { signInWithCredential, GoogleAuthProvider } from "firebase/auth"; import { auth } from '../../app/config/firebase'; WebBrowser.maybeCompleteAuthSession(); // 自定义Hook,接收回调函数处理状态和结果 export function useGoogleAuth({ onStart, onSuccess, onError }) { const [request, response, promptAsync] = Google.useIdTokenAuthRequest( { clientId: 'xxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com', }, ); useEffect(() => { if (response?.type === 'success') { const { id_token } = response.params; const credential = GoogleAuthProvider.credential(id_token); logUser(credential); } else if (response?.type === 'error') { onError?.(new Error('登录失败')); } }, [response]); async function logUser(credential) { try { const result = await signInWithCredential(auth, credential); const user = result.user; onSuccess?.(user); } catch(err) { onError?.(err); } } // 返回触发登录的函数 const signIn = async () => { onStart?.(); if (request) { await promptAsync(); } }; return { signIn }; }
2. 修改LoginScreen.js调用自定义Hook
import React, {useState} from "react"; import { View, StyleSheet } from "react-native"; import { AppTitle, Screen, AppButtonPrimary, ActivityIndicator } from "../../components"; import routes from "../../navigation/routes"; import colors from "../../config/colors"; import { useGoogleAuth } from "../../auth/googleAuth"; // 后续可添加类似的Facebook登录Hook // import { useFacebookAuth } from "../../auth/facebookAuth"; function LoginScreen({ navigation }) { const [loadingPage, setLoadingPage] = useState(false); // 使用Google登录Hook,传入状态回调 const { signIn: googleSignIn } = useGoogleAuth({ onStart: () => setLoadingPage(true), onSuccess: (user) => { setLoadingPage(false); // 登录成功后的逻辑,比如跳转首页 // navigation.navigate(routes.HOME); }, onError: (err) => { setLoadingPage(false); // 处理错误,比如提示用户 console.error(err); } }); // 后续添加Facebook登录的示例 // const { signIn: facebookSignIn } = useFacebookAuth({ // onStart: () => setLoadingPage(true), // onSuccess: (user) => { ... }, // onError: (err) => { ... } // }); return ( <Screen style={styles.container}> <ActivityIndicator visible={loadingPage}/> <AppTitle style={styles.title} size={30}>Connectez-vous à votre compte</AppTitle> <View style={styles.buttons}> <AppButtonPrimary text="Email" icon="envelope-o" onPress={() => navigation.navigate(routes.LOGIN_EMAIL_SCREEN)} width={"80%"} backColor={colors.white} textColor={colors.grayDark} /> <AppButtonPrimary text="Google" icon="google" onPress={googleSignIn} width={"80%"} backColor={colors.white} textColor={colors.grayDark} /> {/* <AppButtonPrimary text="Facebook" icon="facebook" onPress={facebookSignIn} width={"80%"} backColor={colors.white} textColor={colors.grayDark} /> */} </View> </Screen> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", }, title: { marginTop: 30, marginHorizontal:10, textAlign: 'center' }, buttons: { width: "100%", alignItems: "center", marginTop: 20, } }); export default LoginScreen;
关键说明
- 自定义Hook命名以
use开头,符合React Hook规范,确保React能识别为合法的Hook调用。 - 通过
onStart、onSuccess、onError回调,将组件的状态管理(如loading)和登录逻辑解耦,同时保留组件对流程的控制权。 - 其他登录提供商(如Facebook、Apple)可按照相同模式封装为独立的自定义Hook,实现代码模块化和复用。
内容的提问来源于stack exchange,提问作者Maxime Crtgn
相关产品推荐
相关产品推荐

