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:
onAuthStateChanged()
works — it returns the correct user after loginsetPersistence()
is called right after getAuth(app)
getAuth()
anywhere before persistence is setAsyncStorage
is installed and workingI 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
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
}