I have a react firebase Application for authentication. I am using Usecontext along with the setState. You can see the code below. This scenario works fine when user logs in for the first time or signs up. But as soon as I reload, the context value is null for a split second and then it gets the latest value from the auth of firebase. This is causing a problem for me to access the private route even though the user is logged in.
AuthContext.js File
import React, { useContext, useEffect, useState } from "react";
import { firebaseAuth } from "../firebase";
import {
createUserWithEmailAndPassword,
signOut,
signInWithEmailAndPassword,
onAuthStateChanged,
} from "firebase/auth";
const AuthContext = React.createContext();
export const useAuth = () => {
return useContext(AuthContext);
};
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
console.log("Auth Context User:", user);
useEffect(() => {
const unsub = onAuthStateChanged(firebaseAuth, (user) => {
setUser(user);
});
return unsub();
}, [user]);
function SignUp(email, password) {
return createUserWithEmailAndPassword(firebaseAuth, email, password);
}
function Logout() {
return signOut(firebaseAuth);
}
function Login(email, password) {
return signInWithEmailAndPassword(firebaseAuth, email, password);
}
const value = {
logout: Logout,
signup: SignUp,
login: Login,
user,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
App.js File
import Header from "./components/Header";
import Login from "./components/Login";
import NotFound from "./components/NotFound";
import Signup from "./components/Signup";
import Home from "./components/Home";
import AuthGuard from "./guard/AuthGuard";
import ErrorComp from "./components/ErrorComp";
import { AuthContextProvider } from "./context/AuthContext";
import { GlobalStyles } from "./styledComps/Global";
import { Route, Routes } from "react-router-dom";
function App() {
return (
<>
<GlobalStyles />
<AuthContextProvider>
<Header />
<ErrorComp>
<Routes>
<Route path="" element={<Login />} exact />
<Route path="/signup" element={<Signup />} />
<Route
path="/home"
element={
<AuthGuard>
<Home />
</AuthGuard>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</ErrorComp>
</AuthContextProvider>
</>
);
}
export default App;
AuthGuard.js File
import React from "react";
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
const AuthGuard = ({ children }) => {
let { user } = useAuth();
let location = useLocation();
console.log("user", user);
if (!user) {
return <Navigate to="/" state={{ from: location }} />;
}
return children;
};
export default AuthGuard;
Since the user has to get refetched when you reload (which takes some time), the user object will be null until the process has finished. You could simply try to just guard your rendering by
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
console.log("Auth Context User:", user);
useEffect(() => {
const unsub = onAuthStateChanged(firebaseAuth, (user) => {
setUser(user);
});
return unsub();
}, [user]);
function SignUp(email, password) {
return createUserWithEmailAndPassword(firebaseAuth, email, password);
}
function Logout() {
return signOut(firebaseAuth);
}
function Login(email, password) {
return signInWithEmailAndPassword(firebaseAuth, email, password);
}
const value = {
logout: Logout,
signup: SignUp,
login: Login,
user,
};
if ( user !== null ) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
return <>Loading ... or render eg. a spinner</>
};
Related
I'm attempting to move my data fetching to custom hooks off of the component file for better code organization and I'm having issues with the hook not working when used in conjunction with my context.
This is my AuthContext.tsx file
import React, { createContext, useEffect, useState } from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from '../firebase.config';
export const UserContext = createContext<any>({});
export const AuthContextProvider = ({ children }: any) => {
const [currentUser, setCurrentUser] = useState<unknown | null>({});
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => {
setCurrentUser(user);
});
return () => {
unsub();
};
}, []);
return <UserContext.Provider value={{ currentUser }}>{children}</UserContext.Provider>;
};
This is my useRooms.ts file (my hook)
import * as React from 'react';
import { collection, query, where, getDocs, QueryDocumentSnapshot, DocumentData } from 'firebase/firestore';
import { db } from '../firebase.config';
import { UserContext } from '../context/AuthContext';
import { RoomsResult } from './types';
export default function useRooms(): RoomsResult {
const [userRooms, setUserRooms] = React.useState<string[] | null>(null);
const [isLoading, setIsLoading] = React.useState(true);
const { currentUser } = React.useContext(UserContext);
const userRoomsQuery = query(collection(db, 'rooms'), where('user', 'array-contains', currentUser.uid));
const fetchUserRoomsData = async () => {
setIsLoading(true);
const userRoomsDocs = await getDocs(userRoomsQuery);
setUserRooms(userRoomsDocs.docs.map((doc: QueryDocumentSnapshot<DocumentData>) => doc.id));
setIsLoading(false);
};
console.log(userRooms);
return { rooms: userRooms, loading: isLoading, fetchUserRoomsData };
}
The error I recieve is:
As for those file names seen in the error they are all from my App.tsx file here
import React, { useContext } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { UserContext } from './context/AuthContext';
import { SignUpForm, LogInForm, ChatRooms } from './components/exporter';
type Styles = {
wrapper: string;
};
function App() {
const styles: Styles = {
wrapper:
'bg-purple-200 h-[100vh] w-[100vw] grid grid-cols [minmax(100px,_250px)_1fr_minmax(150px,_250px)] grid-rows-[85%_minmax(50px,_350px)] absolute',
};
const { currentUser } = useContext(UserContext);
const ProtectedRoute = ({ children }: any) => {
if (!currentUser) {
return <Navigate to="/login" />;
} else return children;
};
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<ProtectedRoute>
<div id="portal-container">
<ChatRooms />
</div>
</ProtectedRoute>
}
></Route>
<Route path="login" element={<LogInForm />} />
<Route path="signup" element={<SignUpForm />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
);
}
export default App;
The hook itself works as long as I do not use the line const { currentUser } = React.useContext(UserContext); inside the useRooms.ts file, but soon as I do I receive the error in picutre. I place the use of the custom Hook useRooms inside of my <ChatRooms/> component which can be seen is rendered as the most nested element in App.tsx. Any idea as to what is causing this and why ?
I managed to fix the issue by setting the default state of currentUser in AuthContextProvider to null instead of {}
Unfortunately this makes it so that when I refresh my web page it now forgets the logged in user. But this may be intended behavior until I check the Firebase Auth docs.
I am trying to use context with my Gatsby project. I have successfully implemented this in my previous project and I have copied the code over to my new project and it's not working as intended.
This is my context.js file:
import React, { useContext, useState } from "react";
const defaultState = {
isLoggedIn: false,
};
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
function toggle() {
console.log("BOO!");
}
const value = {
isLoggedIn,
setIsLoggedIn,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
This is my app.js file:
import React from "react";
import { Router } from "#reach/router";
import IndexPage from "./index";
import ProjectPage from "./project";
import { AuthProvider } from "../contexts/context";
const App = () => (
<AuthProvider>
<Router basepath="/app">
<IndexPage path="/" component={IndexPage} />
<ProjectPage path="/project" component={ProjectPage} />
</Router>
</AuthProvider>
);
export default App;
This is my index.js file:
import React, { useContext } from "react";
import { Link } from "gatsby";
import { useAuth } from "../contexts/context";
import { AuthContext } from "../contexts/context";
const IndexPage = () => {
console.log(useAuth())
return (
<div className="w-40 h-40 bg-red-400">
{/*<Link to="/project">to projects</Link>*/}
<div>Click me to toggle: uh</div>
</div>
);
};
export default IndexPage;
useAuth() should return the desired components and functions but instead is always returning undefined. I have looked over my previous code as well as snippets on stack overflow and I can't seem to find the correct fix.
The following includes code that successfully built and executed:
Original context.js
import '#stripe/stripe-js'
/* Functionality */
import React, { useContext, useEffect, useState } from "react";
import { navigate } from "#reach/router";
import firebase from 'gatsby-plugin-firebase';
import { useLocalStorage } from 'react-use';
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [isLoading, setIsLoading] = useLocalStorage("loading", false);
// Sign In
const signInWithRedirect = (source) => {
let provider;
switch(source) {
case 'Google':
provider = new firebase.auth.GoogleAuthProvider()
break;
case 'Github':
provider = new firebase.auth.GithubAuthProvider()
break;
default:
break;
}
setIsLoading(true)
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION)
.then(() => {
// Existing and future Auth states are now persisted in the current
// session only. Closing the window would clear any existing state even
// If a user forgets to sign out.
// ...
// New sign-in will be persisted with session persistence.
return firebase.auth().signInWithRedirect(provider)
})
.catch((error) => {
// Handle Errors here.
let errorCode = error.code;
let errorMessage = error.message;
});
}
// Sign Out
const signOut = () => {
firebase.auth().signOut().then(() => {
// Sign-out successful.
setIsLoggedIn(false)
navigate('/app/login')
}).catch((error) => {
// An error happened.
});
}
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
try {
// If user is authenticated
if (!!user) {
// Fetch firestore document reference
var docRef = firebase.firestore().collection("study_guide_customers").doc(user.uid)
docRef.get().then((doc) => {
console.log('checking doc')
// If the document doesn't exist, create it and add to the firestore database
if (!doc.exists) {
console.log('inside customer')
const customer = {
customerCreationTimestamp: firebase.firestore.Timestamp.now(),
username: user.displayName,
email: user.email
}
firebase.firestore().collection("study_guide_customers").doc(user.uid).set(customer)
.then(() => {
// After docuement for user is created, set login status
setIsLoggedIn(!!user)
setIsLoading(false)
})
.catch((error) => {
console.error("Error writing document: ", error);
});
// If document for user exists, set login status
} else {
setIsLoggedIn(!!user)
setIsLoading(false)
}
})
}
} catch {
console.log('Error checking firestore existence and logging in...')
}
})
}, [isLoggedIn, isLoading, setIsLoading, setIsLoggedIn])
const value = {
signOut,
isLoggedIn,
isLoading,
setIsLoading,
setIsLoggedIn,
signInWithRedirect,
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
Original app.js
/* Stripe Security */
import '#stripe/stripe-js'
/* Functionality */
import React from "react"
import { Router } from "#reach/router"
import PrivateRoute from "../components/PrivateRoute"
import Profile from "../components/Profile"
import Login from "../components/Login"
import Projects from "../components/Projects"
import IndexPage from "./index"
import NotFoundPage from './404'
import { AuthProvider } from "../contexts/context"
const App = () => (
<AuthProvider>
<Router basepath="/app">
<PrivateRoute path="/profile" component={Profile} />
<Login path="/login" component={Login}/>
<IndexPage path="/" component={IndexPage}/>
<Projects path="/projects" component={Projects} />
</Router>
</AuthProvider>
)
export default App
Original index.js
/* Stripe Security */
import '#stripe/stripe-js'
/* Functionality */
import * as React from "react"
import IndexContact from "../components/Index/Contact"
import IndexSelectedProjects from "../components/Index/SelectedProjects"
import IndexFeaturedProjects from "../components/Index/FeaturedProjects"
import IndexFooter from "../components/Index/Footer"
import IndexStudyGuide from "../components/Index/StudyGuide"
import IndexNavbar from "../components/Index/Navbar"
import IndexHeader from "../components/Index/Header"
import IndexAbout from '../components/Index/About'
import IndexExperience from '../components/Index/Experience'
import { useMount } from 'react-use';
const IndexPage = () => {
useMount(() => localStorage.setItem('loading', false));
return (
<>
<IndexNavbar />
<IndexHeader />
<IndexAbout />
<IndexExperience />
<IndexFeaturedProjects />
<IndexSelectedProjects />
<IndexStudyGuide />
<IndexContact />
<IndexFooter />
</>
)
}
export default IndexPage
Then in any component I could simply use the following code to access the context
import { useAuth } from "../contexts/context"
const { isLoggedIn, signInWithRedirect, isLoading } = useAuth()
Child components are mounted before parent. Fix your context.js file to add a default value for isLoggedIn state:
const defaultState = {
isLoggedIn: false,
setIsLoggedIn: () => {}
};
const AuthContext = React.createContext(defaultState);
Your defaultState should also include default methods for any parts of the context you wish to work with.
I am new to context api. I am trying to use multiple context.
I have created a UserContext and GlobalContext which I'm trying to use in the app.js but as soon as I put UserContext I get the blank page.
here is my app.js
import { useEffect } from 'react';
import { GlobalProvider } from './context/GlobalState'
import './App.css'
import { Home } from './pages/Home';
import { Login } from './pages/Login';
import { Register } from './pages/Register';
import axios from 'axios';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { UserProvider } from './context/UserContext';
const App = () => {
return (
<UserProvider>
<GlobalProvider>
<BrowserRouter>
<Routes>
<Route path='/login' element={<Login />} />
<Route path='/register' element={<Register />} />
<Route path='/home' element={<Home />} />
</Routes>
</BrowserRouter>
</GlobalProvider>
</UserProvider>
)
}
export default App;
Here is my UserContext.js
import axios from "axios";
import { createContext, useReducer } from "react";
import { userReducer } from "./AppReducer";
// Initial State
const initialState = {
isAuthenticated: false
}
// Creating Context
export const UserContext = createContext(initialState)
UserContext.displayName = "UserContext"
// Creating Provider
export const UserProvider = ({ childern }) => {
const [state, dispatch] = useReducer(userReducer, initialState);
// Actions
async function authenticate(user) {
try {
const config = {
Headers: {
'Content-Type': 'application/json'
}
}
const hasToken = res.data.token === undefined ? false : res.data.token;
dispatch({
type: 'AUTHENTICATE_USER',
payload: hasToken
})
const res = await axios.post('https://localhost:7188/api/user/authenticate', user, config);
if (res.data.token !== undefined) {
console.log("No token")
}
else {
console.log(res.data.token);
}
localStorage.setItem('jwt', res.data.token);
console.log(localStorage.getItem('jwt'));
} catch (error) {
console.log(error.response);
}
}
return (
<UserContext.Provider value={
{
isAuthenticated: state.isAuthenticated,
authenticate
}
}>
{childern}
</UserContext.Provider>
)
}
Here is AppReducer.js
export const userReducer = (state, action) => {
switch (action.type) {
case 'AUTHENTICATE_USER':
return {
...state,
isAuthenticated : action.payload
}
default:
return state;
}
}
When I start the server I'm getting blank page
React Beginner here. I'm building a login form in React using jwt, axios and useContext. After being authorized from the backend, I store the data in the global context using AuthProvider and redirect to home page. the home page first checks for authorization and navigates to login on unauthorized access. However even after updating the auth (useState) on login, I still get a false value on the first click and get sent back to login even if authed. I've tried useEffects everywhere but to no avail. Code below
AuthProvider.jsx
import React, { useState } from "react";
import { createContext } from "react";
const AuthContext = createContext();
export default AuthContext
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [authed, setAuthed] = useState(false);
function login(user) {
setUser(user);
setAuthed(true);
}
return (
<AuthContext.Provider value={{login, user, authed}}>
{children}
</AuthContext.Provider>
)
}
ProtectedRoute.jsx to /home
import React, { useContext, useEffect } from "react";
import AuthContext from "../../context/AuthProvide";
const ProtectedRoute = ({children}) => {
const {login, user, authed} = useContext(AuthContext);
useEffect(() => {
alert("HELLO")
alert(authed)
if (!authed) {
return window.location.href = "/login";
} else {
return children
}
}, [authed, user, login]);
}
export default ProtectedRoute;
Top part of Login.jsx
import React, { useState, useEffect, useRef, useContext } from "react";
import "./login.css";
import AuthContext from "../../context/AuthProvide";
import { axios } from "../../context/axios";
const LOGIN = "/login";
const Login = () => {
const {login, user, authed} = useContext(AuthContext);
const [userEmail, setUserEmail] = useState("");
const [userPassword, setUserPassword] = useState("");
const [error, setError] = useState("");
const errorRef = useRef();
useEffect(() => {
}, [authed, user, login]);
useEffect(() => {
setError("");
}, [userEmail, userPassword]);
function handleUserEmail(event) {
setUserEmail(event.target.value);
}
function handleUserPassword(event) {
setUserPassword(event.target.value);
}
function handleSubmit(event) {
event.preventDefault();
axios.post(LOGIN, {
email: userEmail,
password: userPassword
}).then(response => {
if (response.data.error) {
setError(response.data.error);
} else {
// this is supposed to be the one to set the user and auth to true
login(response.data.token)
alert(authed)
window.location.href = "/";
}
}).catch(error => {
if (!error?.response) {
setError("NO SERVER RESPONSE");
} else if (error.response?.status === 400) {
setError("MISSING USER NAME OR PASSWORD");
} else if (error.response?.status === 401) {
setError("UNAUTHORIZED ACCESS");
} else {
setError("UNKNOWN ERROR");
}
errorRef.current.focus();
})
}
function resetForm() {
setUserEmail("");
setUserPassword("");
}
return (
// the form is here
)
Issues
The main issue I see with the code is the use of window.location.href. When this is used it reloads the page. This remounts the entire app and any React state will be lost/reset unless it is being persisted to localStorage and used to initialize app state.
It is more common to use the navigation tools from react-router-dom (I'm assuming this is the package being used, but the principle translates) to issue the imperative and declarative navigation actions.
Suggestions
The protected route component should either redirect to the login path or render the children prop if user is authorized. It passes the current location being accessed along in route state so user can be redirected back after successful authentication.
import { Navigate } from 'react-router-dom';
const ProtectedRoute = ({ children }) => {
const location = useLocation();
const { authed } = useContext(AuthContext);
if (!authed) {
return <Navigate to="/login" replace state={{ from: location }} />;
} else {
return children;
}
};
The Login component should use the useNavigate hook to use the navigate function to redirect user to protected route once authenticated.
import { useLocation, useNavigate } from 'react-router-dom';
const Login = () => {
const { state } = useLocation();
const navigate = useNavigate();
const { login, user, authed } = useContext(AuthContext);
...
function handleSubmit(event) {
event.preventDefault();
axios.post(
LOGIN,
{
email: userEmail,
password: userPassword
}
)
.then(response => {
if (response.data.error) {
setError(response.data.error);
} else {
login(response.data.token)
navigate(state?.from?.pathname ?? "/", { replace: true });
}
})
.catch(error => {
if (!error?.response) {
setError("NO SERVER RESPONSE");
} else if (error.response?.status === 400) {
setError("MISSING USER NAME OR PASSWORD");
} else if (error.response?.status === 401) {
setError("UNAUTHORIZED ACCESS");
} else {
setError("UNKNOWN ERROR");
}
errorRef.current.focus();
});
}
...
return (
// the form is here
);
}
Wrap the routes you want to protect with the ProtectedRoute component.
<AuthProvider>
<Routes>
...
<Route path="/login" element={<Login />} />
<Route
path="/test"
element={
<ProtectedRoute>
<h1>Protected Test Route</h1>
</ProtectedRoute>
}
/>
</Routes>
</AuthProvider>
When I log in to the page and refresh the page it takes me back to the login page. What I want to happen is that when user is authenticated still it directs from login page to home page and not the other way around and that it stays on home page even page is refreshed.
This is my app.js:
import React from "react"
import { BrowserRouter, Routes, Route, Navigate, useLocation } from "react-router-dom"
import AuthContextProvider from "../Contexts/AuthContext"
import { useAuth } from "../Contexts/AuthContext"
import Login from "../Pages/Authentication/Login"
import Home from "../Pages/Home"
const App = () => {
return (
<AuthContextProvider>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/home"
element={
<RequireAuth>
<Home />
</RequireAuth>
}
/>
</Routes>
</BrowserRouter>
</AuthContextProvider>
)
}
function RequireAuth({ children }) {
const { currentUser } = useAuth()
return currentUser ? children : <Navigate to="/login" replace />
}
export default App
this is my auth context:
import React, { createContext, useContext, useEffect, useState } from "react"
import { auth } from "../firebase/Firebase-config"
import {
signInWithEmailAndPassword,
onAuthStateChanged,
signOut,
} from "firebase/auth"
const AuthContext = createContext({
currentUser: null,
login: () => Promise,
logout: () => Promise,
})
export const useAuth = () => useContext(AuthContext)
export default function AuthContextProvider({ children }) {
const [currentUser, setCurrentUser] = useState(null)
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setCurrentUser(user ? user : null)
})
return () => {
unsubscribe()
}
}, [])
useEffect(() => {
console.log("The user is", currentUser)
}, [currentUser])
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password)
}
function logout() {
return signOut(auth)
}
const value = {
currentUser,
login,
logout,
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
is what the auth context returns
you can use an undefined state if you want.
const [currentUser, setCurrentUser] = useState(undefined)
than the possible states of currentUser will be undefined, null, or the user and you can check for this.
Default it will be undefined so just navigate if it is null.
function RequireAuth({ children }) {
const { currentUser } = useAuth()
if(currentUser) {
return children;
}
if(currentUser === null) {
return <Navigate to="/login" replace />;
}
return null;
}