Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problem with protected routes, context API and firebase user authentication request

I am writing a basic CRUD React app that uses Firebase for authentication. At the moment I am trying to create a protected route for a component named Dashboard. The protected route insures that any encapsulated routes (such as the Dashboard) do not render unless the user is authenticated.If the user is not authenticated then the router redirects to the landing page.

The way that I am accomplishing this is modelled on this article:

I have emulated the pattern in the article above and it works fine. When I incorporate firebase (specifically firebase authentication) my app does not render the Dashboard component even when a user has logged in. Instead it simply redirects to the landning page

I know what the problem is (I think) but I am unsure how to fix it.

The problem is that the call to firebase is an async operation and the dashboard attempts to load before the call to firebase is resolved.

I want to know if their are any tweaks to my code I can make to fix this.

I could make an api call to firebase every time the user loads a protected route (to check for authentication) but I would prefer to set the authentication on the state of the Context and reference that state until the user either logs in or logs out.

I've placed the relevant code below. All files are in the src directory

Thank you!

App.js

import React, { Component } from 'react';
import { BrowserRouter, Route, Redirect } from "react-router-dom";
import {Switch} from 'react-router';
import Landing from './PageComponents/Landing';
import {Provider} from './PageComponents/Context';
import Dashboard from './PageComponents/Dashboard';
import ProtectedRoute from './PageComponents/ProtectedRoute';

class App extends Component {
  render() {
    return (
      <div className="App">

      <Provider>
        <BrowserRouter>

        <div>
          <Switch>

            <Route exact={true} path="/" component={Landing} />
            <ProtectedRoute exact path="/dashboard" component={Dashboard} /> 

          </Switch>
        </div>

        </BrowserRouter>
        </Provider>

      </div>
    );
  }
}

export default App;


PageComponents/Context.js

import React from 'react';
import { getUser } from '../services/authentication';

let Context = React.createContext();

class Provider extends React.Component {

    state = {
        userID: true,
        user:undefined,
        authenticated:false
    }

    async getUser(){

        try{

            let user = await getUser();

            return user

        } catch(error){

            console.log(error.message)
        }

    }


    async componentDidMount(){

        console.log("waiting to get user")
        let user = await this.getUser();
        console.log(user)
        console.log("got user")

        this.setState({
          userID: user.uid,
          user:user,
          authenticated:true
        })
    }


    render(){
        console.log(this.state)
        return(
            <Context.Provider value={{   
                 state:this.state
            }}>
                {this.props.children}
            </Context.Provider>
        )
    }
}

const Consumer = Context.Consumer;

export {Provider, Consumer};

PageComponents/Dashboard

import * as React from 'react';
import {Consumer} from '../../PageComponents/Context';



class Dashboard extends React.Component {
  render(){

    console.log("Dashboard component loading....")

     return(


        <Consumer>
            {(state)=>{
                console.log(state)
                return(
                  <div>
                    <p> Dashboard render</p>
                  </div>
                )
             }}
        </Consumer>
     )
  }
}

export default Dashboard

PageComponents/ProtectedRoute


import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { Consumer } from '../PageComponents/Context';


const ProtectedRoute = ({ component: Component, ...rest }) => {
    console.log("Middleware worked!");

    return (

        <Consumer>
               {(context)=>{
                /*________________________BEGIN making sure middleware works and state is referenced */
                  console.log(context);
                  console.log("Middle ware");
                /*________________________END making sure middleware works and state is referenced */

                 console.log(  context.getState().authenticated + " <--- is authenticated from ProtectedRoute. true or false?")

                  if(context.state.authenticated){

                    return(

                        <Route {...rest} render={renderProps => {
                           console.log(renderProps);
                           return (<Component {...renderProps} />)
                        }}/>

                    )  

                  }else{

                    return <Redirect to="/"/>

                  }

                }}

        </Consumer>
    )


};

export default ProtectedRoute;




services/authentication

import firebase from '../../services/firebase'




const getUser = () =>{

    return new Promise((resolve, reject) => {   // Step 3. Return a promise

         //___________________ wrapped async function

         firebase.auth().onAuthStateChanged((user)=> {

                if(user){

                    resolve(user);   //____This is the returned value of a promise

                 }else{

                   reject(new Error("Get user error"))

                }
         })

       //_____________________END wrapped async function  

    });

}


export {getUser }


like image 428
William Avatar asked Feb 08 '19 04:02

William


1 Answers

Problem: You are indeed correct, the API call to getUser is async and is trigged in componentDidMount, hence by the time authentication state is set to true, the Redirect component has already been triggered.

Solution: What you need to wait till the authentication request is a success and then take a decision to load the Route or redirect in ProtectedRoute component.

In order to make it work, you need a loading state.


PageComponents/Context.js

let Context = React.createContext();

class Provider extends React.Component {

    state = {
        userID: true,
        user:undefined,
        loading: true,
        authenticated:false
    }

    async getUser(){
       let user = await getUser();
       return user

    }


    async componentDidMount(){
        console.log("waiting to get user")
        try {
          let user = await this.getUser();
          console.log(user)
          console.log("got user")

          this.setState({
            userID: user.uid,
            user:user,
            loading: false,
            authenticated:true
          })
       } catch(error) {
           console.log(error);
           this.setState({
               loading: false,
               authenticated: false
           })
       }
    }


    render(){
        return(

            <Context.Provider value={{
                 state:this.state
            }}>
              {this.props.children}
            </Context.Provider>

        )
    }
}


const Consumer = Context.Consumer;

export {Provider, Consumer}

PageComponents/ProtectedRoute

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { Consumer } from '../PageComponents/Context';


const ProtectedRoute = ({ component: Component, ...rest }) => {
    console.log("Middleware worked!");
    return (
        <Consumer>
            {(context)=>{
                /* BEGIN making sure middleware works and state is referenced */
                  console.log(context);
                  console.log("Middle ware");
                /* END making sure middleware works and state is referenced */

                 console.log(  context.getState().authenticated + " <--- is authenticated from ProtectedRoute. true or false?")

                 // Show loading state 
                 if (context.state.loading) {
                     return <Loader /> 
                 }
                 if(context.state.authenticated){
                    return(
                        <Route {...rest} render={renderProps => {
                           console.log(renderProps);
                           return (<Component {...renderProps} />)
                        }}/>
                    )  
                  }else{
                    return <Redirect to="/"/>
                  }
                }}
        </Consumer>
    )
};

export default ProtectedRoute;
like image 122
Shubham Khatri Avatar answered Oct 07 '22 03:10

Shubham Khatri