javascriptfirebasereact-nativereact-reduxredux-thunk

Firebase authentication Redux useSelector


I want to redirect to the next screen when signIn succefull, however I'm having issues with the redux implementation. I'm able to complete the signIn/signUp and get the current user but in the file that is supposed to contain the routes, I'm not getting the user info. When I succesfull sigIn, the screen remains the same. My repo: https://github.com/carlos-ediaz/tap.git

App.jsx code:

import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import React from "react";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import rootReducer from "./src/redux/reducers";
import { thunk } from "redux-thunk";

import Route from "./src/navigation/main";

const store = createStore(rootReducer, applyMiddleware(thunk));

export default function App() {
  return (
    <Provider store={store}>
      <Route />
    </Provider>
  );
}

My index.js file (Exports :

import { View, Text } from "react-native";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getCurrentUserInfo} from "../../redux/actions";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import AuthScreen from "../../screens/auth";
import HomeScreen from "../home";
import { getAuth, onAuthStateChanged } from "firebase/auth";
import { fdb } from "../../../db";

const Stack = createNativeStackNavigator();
const auth = getAuth(fdb);
export default function Route() {
  const currentUserObj = useSelector((state) => state.auth);
  const dispatch = useDispatch();

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      console.log("Executing dispatch");
      if (user) {
        dispatch(getCurrentUserInfo());
      } else {
        dispatch({
          type: USER_STATE_CHANGE,
          currentUser: null,
        });
      }
    });
    return unsubscribe;
  }, []);

  if (!currentUserObj?.loaded) {
    return (
      <View>
        <Text>index</Text>
      </View>
    );
  }

  return (
    <NavigationContainer>
      <Stack.Navigator>
        {currentUserObj.currentUser ? (
          <Stack.Screen name="home" component={HomeScreen} options={{ headerShown: false }}/>
        ) : (
          <Stack.Screen name="auth" component={AuthScreen} options={{ headerShown: false }} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

My redux/actions file:

import { collection, doc, getDoc, onSnapshot, getFirestore, setDoc } from "firebase/firestore";
import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword, onAuthStateChanged, } from "firebase/auth";
import { fdb } from "../../../db";
import { USER_STATE_CHANGE } from "../constants";

const auth = getAuth(fdb);

export const userAuthStateListener = () => (dispatch) => {
  onAuthStateChanged(auth, (user) => {
    if (user) {
      dispatch(getCurrentUserInfo());
    } else {
      dispatch({ type: USER_STATE_CHANGE, currentUser: null, loaded: true });
    }
  });
};

export const getCurrentUserInfo = () => async (dispatch) => {
  const db = getFirestore(fdb);
  const userRef = collection(db, "user");
  const docRef = doc(userRef, auth.currentUser.uid);
  const res = await getDoc(docRef);
  console.log(res.data());
  return dispatch({
    type: USER_STATE_CHANGE,
    currentUser: res.exists ? res.data() : null,
    loaded: true,
  });
};

export const login = (email, password) => (dispatch) =>
  new Promise((resolve, reject) => {
    signInWithEmailAndPassword(auth, email, password)
      .then(() => {
        resolve();
      })
      .catch((error) => {
        reject(error);
      });
  });

export const register = (email, password) => (dispatch) =>
  new Promise((resolve, reject) => {
    createUserWithEmailAndPassword(auth, email, password)
      .then(() => {
        resolve();
      })
      .catch((error) => {
        reject(error);
      });
  });

My redux/reducers/auth file:

import { USER_STATE_CHANGE } from "../constants";

const initialState = {
  currenUser: null,
  loaded: false,
};

export const auth = (state = initialState, action) => {
  switch (action.type) {
    case USER_STATE_CHANGE:
      return {
        ...state,
        currenUser: action.currenUser,
        loaded: action.loaded,
      };
    default:
      return state;
  }
};

My redux/reducers/index file:

import { combineReducers } from "redux";
import { auth } from "./auth";
const Reducers = combineReducers({
  auth,
});

export default Reducers;

constants file:

export const USER_STATE_CHANGE = 'USER_STATE_CHANGE'

I have put a lot of console.log but there's something with the function becasue the useEffect only calls the first time and if i put ...},[currentUserObj]); calls the function all the time but doesnt update the user info.

I'm done. Not sure what to do. really stuck


Solution

  • You appear to not quite use the Firebase handlers and Redux correctly.

    onAuthStateChanged only needs to be called once with the appropriate callbacks passed to it to handle authentication changes. Instantiate a call to onAuthStateChanged in a useEffect hook and return a cleanup function to unsubscribe the listener.

    Example:

    const auth = getAuth(fdb);
    
    export default function Route() {
      const dispatch = useDispatch();
    
      const currentUserObj = useSelector((state) => state.auth);
    
      // Mounting effect to instantiate the auth listener
      useEffect(() => {
        const unsubscribe = onAuthStateChanged(auth, (user) => {
          if (user) {
            dispatch(getCurrentUserInfo());
          } else {
            dispatch({
              type: USER_STATE_CHANGE,
              currentUser: null,
            });
          }
        });
    
        // Unsubscribe when component unmounts
        return unsubscribe;
      }, []);
    
      if (!currentUserObj?.loaded) {
        return (
          <View>
            <Text>No loaded</Text>
          </View>
        );
      }
    
      return (
        <NavigationContainer>
          <Stack.Navigator>
            {currentUserObj.currentUser ? (
              <Stack.Screen name="home" component={HomeScreen} />
            ) : (
              <Stack.Screen name="auth" component={AuthScreen}/>
            )}
          </Stack.Navigator>
        </NavigationContainer>
      );
    }
    

    Similarly, the getCurrentUserInfo action only needs to trigger getting the current user's information once. onSnapshot instantiates a listener, but you likely don't want to do this, especially since you don't clean them up, e.g. there is no logic to unsubscribe from changes. You could use getDoc to fetch the data once.

    Example update:

    export const getCurrentUserInfo = () => async (dispatch) => {
      const db = getFirestore(fdb);
      const docRef = doc(db, "users", auth.currentUser.uid);
      const doc = await getDoc(docRef);
    
      return dispatch({
        type: USER_STATE_CHANGE,
        currentUser: doc.exists ? doc.data() : null,
      });
    };
    

    Handle the "isLoading" and "loaded" states in the reducer(s) when processing the dispatched actions.