Hitman DD
Hitman DD

Reputation: 67

How to make a Private Route without AuthContext

I'm trying to make a private route from my login and signup pages to my dashboard page, but all the tutorials and guides that I've stumbled upon all require some sorta AuthContext thing and I didn't implement my authentication procedure using AuthContext.
I've tried different ways but none of them work and just end up giving me a blank page when I get to the dashboard page, what can I do to make it a private route? Using Firebase v9 btw.

SignUp.js

import './Signup.css'
import React, { useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { auth }from '../../../firebase';
import { createUserWithEmailAndPassword, onAuthStateChanged } from 'firebase/auth';
import { Typography, Container, TextField, Button, Alert } from '@mui/material';

const Signup = () => {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [confirmPassword, setConfirmPassword] = useState('');
    const [error, setError] = useState('');
    const [user, setUser] = useState({});
    const history = useHistory();

    onAuthStateChanged(auth, (currentUser) => {
        setUser(currentUser);
    })
    
    const signup = async (e) => {
        e.preventDefault();

        if (password !== confirmPassword) {
            return setError("Passwords do not match")
        }

        try {
            const user = await createUserWithEmailAndPassword(
                auth, 
                email,
                password
            );
            history.push("/dashboard/canvas");
        } catch (err) {
            setError(err.message);
        }
    }

    return (
        <>
            <div className="text-div">
                <Typography textAlign="center" variant="h3">Create a new account</Typography>
            </div>
            
            <Container className="cont" maxWidth="xl" sx={{ backgroundColor: "#ffffff", width: 500, height: "auto", borderRadius: 4, marginTop: 5, display: "flex", flexDirection: "column", padding: 5, }}>
                { error && <Alert severity="error">{error}</Alert> }
                <TextField label="Email" margin="dense" type="email" onChange={ (e) => {
                    setEmail(e.target.value);
                }}/>

                <TextField label="Password" margin="dense" type="password" onChange={ (e) => {
                    setPassword(e.target.value);
                }}/>

                <TextField label="Confirm Password" margin="dense" type="password" onChange={ (e) => {
                    setConfirmPassword(e.target.value);
                }}/>
                <Button onClick={signup} variant="contained" sx={{ marginTop: 2, }}>Sign Up</Button>

                <div>
                    Already have an account? <Link to="/login" style={{ color: '#000' }}>Log In</Link>
                </div>
            </Container>
        </>
    )
}

export default Signup;

Login.js

import './Login.css'
import React, { useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth }from '../../../firebase';
import { Typography, Container, TextField, Button, Alert } from '@mui/material';


const Login = () => {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [error, setError] = useState('');
    const history = useHistory();

    const login = async () => {
        try {
            const user = await signInWithEmailAndPassword(
                auth, 
                email,
                password
            );
            //alert("Success, user is recognized");
            history.push("/dashboard/canvas");
        } catch (err) {
            setError("The email or password you entered is incorrect");
        }
    }

    return (
        <>
            <div className="text-div">
                <Typography textAlign="center" variant="h3">Login</Typography>
            </div>
            
            <Container className="cont" maxWidth="xl" sx={{ backgroundColor: "#ffffff", width: 500, height: "auto", borderRadius: 4, marginTop: 5, display: "flex", flexDirection: "column", padding: 5, }}>
                { error && <Alert severity="error">{error}</Alert> }
                <TextField label="Email" margin="dense" type="email" onChange={(e) => {
                    setEmail(e.target.value);
                }}/>

                <TextField label="Password" margin="dense" type="password" onChange={(e) => {
                    setPassword(e.target.value);
                }}/>

                <Button onClick={login} variant="contained" sx={{ marginTop: 2, }}>Login</Button>

                <div>
                    Don't have an account? <Link to="/signup" style={{ color: '#000' }}>Create one here</Link>
                </div>
                <div>
                    <Link to="/request-password-reset" style={{ color: '#000' }}>Forgot your password?</Link>
                </div>
            </Container>
        </>
    )
}

export default Login;

firebase.js

import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';

const firebaseConfig = {
    apiKey,
    authDomain,
    projectId,
    storageBucket,
    messagingSenderId,
    appId,
    measurementId
}

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

export {
    auth
};

App.js

import './App.css';
import Home from './components/Pages/Home';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Signup from './components/Pages/Signup/Signup';
import Login from './components/Pages/Login/Login';
import UserDashboard from './components/Pages/UserDashboard/UserDashboard';
import ForgotPassword from './components/Pages/Forgot-Password';

function App() {
  return (
      <Router>
          <div>
            <Switch>
              <Route exact path="/" component={Home}/>
              <Route path="/signup" component={Signup}/>
              <Route path="/login" component={Login}/>
              <Route path="/dashboard" component={UserDashboard}/>
              <Route path="/request-password-reset" component={ForgotPassword}/>
            </Switch>
          </div>
      </Router>
  );
}

export default App;

Upvotes: 1

Views: 1210

Answers (1)

Drew Reese
Drew Reese

Reputation: 202854

If you are trying to create a private route component without persisting the authentication state somewhere in your app and exposed out via a React context then you will need to check the auth status asynchronously on each route change. This means you'll also need a "loading" or "pending" state while the auth status check occurring.

Here's an example implementation of just a custom private route sans any persisted state.

import { useEffect, useState } from 'react';
import { Route, Redirect } from 'react-router-dom'; // v4/5
import { onAuthStateChanged } from 'firebase/auth';
import { auth }from '../../../firebase';

const PrivateRoute = props => {
  const [pending, setPending] = useState(true);
  const [currentUser, setCurrentUser] = useState();

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(
      auth,
      user => {
        setCurrentUser(user);
        setPending(false);
      },
      error => {
        // any error logging, etc...
        setPending(false);
      }
    );

    return unsubscribe; // <-- clean up subscription
  }, []);

  if (pending) return null; // don't do anything yet

  return currentUser 
    ? <Route {...props} />      // <-- render route and component
    : <Redirect to="/login" />; // <-- redirect to log in
};

react-router-dom@6

Custom route components are out in v6, use a layout route. The PrivateRoute component will replace Route with Outlet for nested routes to render their matched element prop into, and Navigate replaces Redirect.

import { useEffect, useState } from 'react';
import { Outlet, Navigate } from 'react-router-dom'; // v4/5
import { onAuthStateChanged } from 'firebase/auth';
import { auth }from '../../../firebase';

const PrivateRoute = props => {
  const [pending, setPending] = useState(true);
  const [currentUser, setCurrentUser] = useState();

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(
      auth,
      user => {
        setCurrentUser(user);
        setPending(false);
      },
      error => {
        // any error logging, etc...
        setPending(false);
      }
    );

    return unsubscribe; // <-- clean up subscription
  }, []);

  if (pending) return null; // don't do anything yet

  return currentUser 
    ? <Outlet />                        // <-- render outlet for routes
    : <Navigate to="/login" replace />; // <-- redirect to log in
};

Wrap the routes you want to protect.

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/signup" element={<Signup />} />
        <Route path="/login" element={<Login />} />
        <Route path="/request-password-reset" element={<ForgotPassword />} />
        <Route element={<PrivateRoute />}>
          <Route path="/dashboard" element={<UserDashboard />} />
        </Route>
      </Routes>
    </Router>
  );
}

Upvotes: 6

Related Questions