Using if statement to determine if a Firebase User can access certain React Router paths

Viewed 519

I've got a React web app with routes defined via React Router.

The auth on the app is done with Firebase, and I want to protect certain routes depending upon whether a user is logged in or not.

To determine if the user is logged in, I have used the following function:

let user = firebase.auth().currentUser;

    if (user) {

    } else {

    }
  }

From there, I figured that I could put 'protected' routes in to the initial if as that would determine whether the user was logged in, like so:

    let user = firebase.auth().currentUser;

    if (user) {
      <div>
      <Route path="/dashboard" component={Dashboard} exact />
      </div>

    } else {
      <Route path="/sign-in" component={SignIn} />
    }
  }

If the user isn't logged in, then they'd be re-directed to the sign in component.

This however isn't working. The dashboard page just comes back as a blank white screen instead of redirecting.

Indeed, an error message is displayed in the console:

Warning: Failed prop type: Invalid prop children supplied to Switch, expected a ReactNode.

For reference, here is my full component:

class App extends Component {
  constructor(props) {
    super(props)

    this.userLoggedIn = this.userLoggedIn.bind(this)
    
  }


  userLoggedIn = () => {
    let user = firebase.auth().currentUser;

    if (user) {
      <div>
      <Route path="/dashboard" component={Dashboard} exact />
      </div>
    } else {
      <Route path="/sign-in" component={SignIn} />
    }
  }

  render() {
    return (
      <div>

      <Switch>

      {this.userLoggedIn}

      <Route exact path="/" component={Homepage}  />
      <Route path="/sign-in" component={SignIn} />
      <Route path="/register" component={Register} />

      </Switch>

      </div>
    )
  }
}

I know the logic is flawed, but is there a way I could refactor my code to implement this really straightforward hack to get protected routes using Firebase Auth?

Hope that's clear, let me know if any more info is needed.

EDIT: Edited my code in line with answer, but now receiving the following error message:

TypeError: Right side of assignment cannot be destructured
PrivateRoute
src/PrivateRoute.js:8
   5 | import { AuthContext } from './Auth'
   6 | 
   7 | export default function PrivateRoute({ component: RouteComponent, ...rest }) {
>  8 |     const {currentUser} = useContext(AuthContext)
   9 |     return (
  10 |         <Route
  11 |         {...rest}

My updated code...

Auth.js

import React, { useEffect, useState } from 'react'
import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import firebaseConfig from './firebaseConfig';


export const AuthContext  = React.createContext();

export function AuthProvider({ children }) {
    const  [currentUser, setCurrentUser] = useState(null);

    useEffect(() => {
        firebase.auth().onAuthStateChanged(setCurrentUser);
    }, []);

    return (
        <AuthContext.Provider
        value={{currentUser}}
        >
            {children}
        </AuthContext.Provider>
    )
}

PrivateRoute.js

import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { AuthContext } from './Auth'

export default function PrivateRoute({ component: RouteComponent, ...rest }) {
    const { currentUser } = useContext(AuthContext)
    return (
        <Route
        {...rest}
        render={routeProps =>
        !!currentUser ? (
            <RouteComponent {...routeProps} currentUser={currentUser} />
        ) : (
            <Redirect to={'/signin'}/>
        )
        }
        />
    )
}

App.js

import React, { Component } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import PrivateRoute from './PrivateRoute';
import './App.css';
import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import firebaseConfig from './firebaseConfig';


class App extends Component {

  render() {
    return (
      <div>

      <Switch>

      <PrivateRoute path="/dashboard" component={Dashboard} />
      <Route exact path="/" component={Homepage}  />
      <Route path="/sign-in" component={SignIn} />
      <Route path="/register" component={Register} />

      </Switch>

      </div>
    )
  }
}

export default App;
2 Answers

there are 2 ways of fixing this and cleaning it up a little.

Let's say you want only authenticated users to access the dashboard page

The Easy Option:

class App extends Component {
  
  state = {
    currentUser: undefined
  }

  componentDidMount(){
    const user = firebase.auth().currentUser
    this.setState({currentUser: user})
  }

render() {
    return (
      <div>

      <Switch>

      <Route exact path="/dashboard">
        {this.state.user ? (
          <Dashboard />
        ) : (
          <Redirect to={"/login"} />
        )}
      </Route>

      </Switch>

      </div>
    )
  }
}

A couple of notes:

  • It's best to get the current user and setting any initial state when the component mounts using componentDidMount().
  • Instead of using the constructor React supports simply just declaring initial state as I have
  • Lastly, react-router has a <Redirect to={path}/> component, which is great for use cases such as checking for an authenticated user.

The Cleaner (and my preferred option):

If you want to abstract the authentication process and make your life simpler in the long term you can create 2 files (e.g. base.js, Auth.js and PrivateRoute.js)

  • base.jsis where we are going to initialise firebase

    import * as firebase from 'firebase/app'
    
      const app = firebase.initializeApp({
          apiKey: process.env.REACT_APP_FIREBASE_KEY,
          authDomain: process.env.REACT_APP_FIREBASE_DOMAIN,
          databaseURL: process.env.REACT_APP_FIREBASE_DATABASE,
          projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
          storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
          messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID,
          measurementId: process.env.REACT_APP_MEASUREMENT_ID
      });
    
      export default app;
    

Notes:

  • I have put process.env...... as the values for the API keys as it's always best practice to keep your keys in a .env file.

  • Auth.js is where we are going to be handling getting the authenticated user when the authenticated state changes

Auth.js:

import React, { useEffect, useState } from 'react'
import app from './base'

export const AuthContext  = React.createContext();

export function AuthProvider({ children }) {
    const  [currentUser, setCurrentUser] = useState(null);

    useEffect(() => {
        app.auth().onAuthStateChanged(setCurrentUser);
    }, []);

    return (
        <AuthContext.Provider
        value={{currentUser}}
        > 
            {children}
        </AuthContext.Provider>
    )
}

Notes:

  • Don't forget to import React, useState, useEffect and firebase
  • This file will provide an authentication context so that you can grab it from anywhere in your React App

PrivateRoute.js:

import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { AuthContext } from './Auth'

export default function PrivateRoute({ component: RouteComponent, ...rest }) {
    const currentUser = useContext(AuthContext)
    return (
        <Route
        {...rest}
        render={routeProps =>
        !!currentUser ? (
            <RouteComponent {...routeProps} currentUser={currentUser} />
        ) : (
            <Redirect to={'/login'}/>
        )
        }
        />
    )
}

Notes:

  • Over here we are importing the authentication context from the Auth.js file and getting the current user.
  • We are then creating a PrivateRoute, which checks if there is a current user and if so using Route and Redirect from the react-router-dom package we can redirect a user to the /login page if they are not authenticated or to the RouteComponent that is passed along with additional props (if there are any).
  • We are also passing along the currentUser in the RouteComponent as props so it's available in that component.

With this, you can now use the PrivateRoute component like this to make sure only authenticated users can use a given component:

import PrivateRoute from "./PrivateRoute";
import Dashboard from "./Dashboard"

<PrivateRoute exact path="/dashboard" component={Dashboard} />

this.userLoggedIn is a function, and as such it must return the JSX you want it to render:

  userLoggedIn = () => {
    let user = firebase.auth().currentUser;

    if (user) {
      return (<div>
        <Route path="/dashboard" component={Dashboard} exact />
      </div>);
    } else {
      return <Route path="/sign-in" component={SignIn} />;
    }
  }

Also in your render function, you want to call the function this.userLoggedIn, not just drop it in as a value. Otherwise it won't do anything:

  render() {
    return (
      <div>

      <Switch>

      {this.userLoggedIn()}

      <Route exact path="/" component={Homepage}  />
      <Route path="/sign-in" component={SignIn} />
      <Route path="/register" component={Register} />

      </Switch>

      </div>
    )
  }
Related