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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 17:31:09