javascriptreactjsreact-nativefirebase-authenticationexpo

How can I persist Firebase user login session using Expo and JavaScript in React Native? Still redirects to Start screen after app refresh


I’m building a React Native app using Expo and JavaScript (no TypeScript) and integrating Firebase for authentication. After a user logs in successfully, I want them to stay logged in even after closing and reopening the app.

However, after an app refresh or restart, the user is redirected to the Start screen again and the user is set to NULL — even though they had just logged in before and outputting their values in the console. Here’s what I’ve tried and my current setup:

What I’ve Verified / Tried:

I feel like everything is wired correctly, but the session doesn’t persist — is there something I’m missing with setPersistence() or a race condition with how Firebase initializes?

App.js

import { createStackNavigator } from '@react-navigation/stack';
import { NavigationContainer } from '@react-navigation/native';
import Start from './screens/Start'
import Home from './screens/Home'
import Register from './screens/Register'
import Login from './screens/Login'
import TermsAndConditions from './screens/TermsAndConditions'
import useAuth from './hooks/useAuth';

const Stack = createStackNavigator()

export default function App() {
  const { user, loading } = useAuth();
  console.log("User:", user)

  if (loading) {
    return null;
  }

  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName={user ? 'Home' : 'Start'}>
        {user ? (
          <>
            <Stack.Screen
              name='Home'
              component={Home}
              options={{ headerShown: false }}
            />
            <Stack.Screen
              name='TermsAndConditions'
              component={TermsAndConditions}
              options={{ headerShown: false }}
            />
          </>
        ) : (
          <>
            <Stack.Screen
              name='Start'
              component={Start}
              options={{ headerShown: false }}
            />
            <Stack.Screen
              name='Register'
              component={Register}
              options={{ headerShown: false }}
            />
            <Stack.Screen
              name='Login'
              component={Login}
              options={{ headerShown: false }}
            />
            <Stack.Screen
              name='TermsAndConditions'
              component={TermsAndConditions}
              options={{ headerShown: false }}
            />
          </>
        )}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

firebase.js:

import { initializeApp } from 'firebase/app';
import { initializeAuth, getReactNativePersistence } from 'firebase/auth';
import AsyncStorage from "@react-native-async-storage/async-storage"

const firebaseConfig = {
.... my config here ....
};

export const app = initializeApp(firebaseConfig);

export const auth = initializeAuth(app, { 
  persistence: getReactNativePersistence(AsyncStorage) 
});

useAuth.js:

import { useEffect, useState } from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from '../config/firebase';
 
export default function useAuth() {
  const [user, setUser] = useState({ user: null, loading: true });

  useEffect(() => {
    const unsub = onAuthStateChanged(auth, (user) => {
      console.log("Got user:", user)
      if (user) {
        setUser({ user, loading: false })
      } else {
        setUser({ user: null, loading: false })
      }
    });
    return unsub
  }, []);
  return user;
}

Let me know if you need Login.js or my Register.js for more context


Solution

  • Your hook will always return { user: null } on the first render because it will still be waiting for the callback of onAuthStateChanged to be called.

    What I would suggest is doing something like:

    export default function useAuth() {
      const [user, setUser] = useState({ user: null, loading: true });
    
      useEffect(() => {
        const unsub = onAuthStateChanged(auth, (user) => {
          console.log("Got User:", user);
          if (user) {
            setUser({ user, loading: false })
          } else {
            setUser({ user: null, loading: false })
          }
        });
        return unsub
      }, []);
      return user;
    }
    
    

    and then you can check if the hook is still loading:

    export default function App() {
      const { user, loading } = useAuth();
      console.log('User:', user);
      if (loading) { return /* a loader of some kind */ }
      // ... rest of the method here
    }