I have a problem with flickering my private routes while using my AuthContext. Below is the code for my Private Route:
import React from 'react';
import { Navigate } from 'react-router-dom';
import { UserAuth } from '../../Context/AuthContext';
const PrivateRoute = ({ children }) => {
const { user } = UserAuth();
if (!user) {
return <Navigate to='/login' />;
}
return children;
};
export default PrivateRoute;
No personal information shows, because the user is initialized to {} in Auth Context. but I can still see the page and navbar. Anyone have a solution?
Also, below is AuthContext.js:
import { createContext, useContext, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
} from 'firebase/auth';
import { auth } from '../../firebase';
const UserContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState({});
const createUser = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password);
};
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password)
}
const logout = () => {
return signOut(auth)
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
//console.log(currentUser);
setUser(currentUser);
});
return () => {
unsubscribe();
};
}, []);
return (
<UserContext.Provider value={{ createUser, user, logout, signIn }}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};
So I found a solution that's kind of cheeky. I'm not going to post my solution, but basically, I wrapped the return statement return children in the PrivateRoute function with an if statement for a specific item in the user object. This prevents any return and 'solves' the flicker.
Related
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>
This question already has answers here:
How to create a protected route with react-router-dom?
(5 answers)
Closed last year.
I have 2 routes that each one is for a type of user, how do I do it differently, I already have my auth, but I don't know how to do that. If anyone knows or has seen any documentation on this just put it below, I'm finding this all day
import React, { createContext, useState, useEffect } from 'react'
import { useNavigate } from "react-router-dom";
import axios from '../services/api';
export const AuthContext = createContext()
export const AuthProvider = ({ children }) => {
const navigate = useNavigate();
const [user, setUser] = useState()
const [loading, setLoading] = useState(true)
useEffect(() => {
const recoveredUser = localStorage.getItem('user')
if (recoveredUser) {
const jsonUser = JSON.parse(recoveredUser)
axios.defaults.headers.common['Authorization'] = `Bearer ${jsonUser.token}`;
setUser(jsonUser)
navigate('/user')
}
setLoading(false)
}, [])
const login = (data) => {
if (data) {
localStorage.setItem('user', JSON.stringify(data))
setUser(data)
navigate('/user')
}
}
const logout = () => {
localStorage.removeItem('user')
setUser("")
navigate('/login')
}
return (
<AuthContext.Provider value={{ authenticated: !!user, user, loading, login, logout }}>
{children}
</AuthContext.Provider>
)
}
Kent C Dodds has a really good article about this and its what I follow in my apps.
https://kentcdodds.com/blog/authentication-in-react-applications
Because you have your user in your context you can just check that where your routes get rendered.
import { Routes } from 'react-router-dom'
import { useAuth } from './context/auth'
function App() {
const { user } = useAuth();
return user ? <AuthenticatedApp /> : <UnauthenticatedApp />
}
function AuthenticatedApp () {
return (
<Routes>
// all your authenticated routes
</Routes>
)
}
function UnauthenticatedApp () {
return (
<Routes>
// all your un authenticated routes
</Routes>
)
}
In my example I created a useAuth() hook which you would have to do in your file.
function useAuth() {
const context = useContext(AuthContext);
return context;
}
export { useAuth };
I am trying to figure out how to mock the google sign in popup method using jest.
Here is the auth file
import { firebase, googleProvider } from "./firebase";
import React, { createContext, useContext, useEffect, useState } from "react";
export const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setUser] = useState(null);
const [isAuthenticating, setIsAuthenticating] = useState(true);
function startLogin() {
return firebase.auth().signInWithPopup(googleProvider);
}
function logOut() {
firebase
.auth()
.signOut()
.then(() => {
setUser(null);
});
}
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
}
setIsAuthenticating(false);
});
return unsubscribe;
}, [currentUser]);
const value = {
currentUser,
isAuthenticating,
startLogin,
logOut,
};
return (
<AuthContext.Provider value={value}>
{!isAuthenticating && children}
</AuthContext.Provider>
);
}
The login method is called through the login component
import React from "react";
import { useAuth } from "../auth/auth";
import { history } from "../routes/AppRouter";
import logo192 from "../images/logo192.png";
import Button from "#material-ui/core/Button";
import Box from "#material-ui/core/Box";
import Typography from "#material-ui/core/Typography";
import { useStyles } from "../styles/LoginStyle";
export const Login = () => {
const { startLogin } = useAuth();
const login = async () => {
try {
await startLogin();
history.push("/welcome");
} catch (e) {
throw new Error(e);
}
};
const classes = useStyles();
return (
<Box className={classes.loginDiv}>
<img src={logo192} alt="react-logo" className={classes.reactLogo} />
<Box className={classes.welcomeText}>
<Typography variant="h3">React Quiz App</Typography>
<Typography variant="h5">
Take 10 random questions and find out how smart you are
</Typography>
</Box>
<Button
data-testid="login-button"
className={classes.login}
onClick={login}
>
LOGIN
</Button>
</Box>
);
};
Now I want to test (mock the method I think) using jest so something like the below
Not sure if i should test by importing the login component or the auth.js file?
describe("firebase auth methods", () => {
const container = document.createElement("div");
document.body.appendChild(container);
ReactDOM.render(
<AuthContext.Provider value={{}}>
<Login />
</AuthContext.Provider>,
container
);
it("should call google sign in with popup", () => {
///call the sign in with pop method here
});
});
Any help/advice would be appreciated :D
I'm currently attempting to build an 'AuthContext' so I can use it in various screens and pass the data down.
I thought I'd built it right.. But when I try to call one of the functions in my Provider, it's throwing a component exception, stating 'element type is invalid: expected a string or a class/function but got undefined'.
Here is the context file:
import React, { useState, useContext } from 'react';
import { navigate } from '../navigationRef';
import { Magic } from '#magic-sdk/react-native';
const m = new Magic('api key');
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState([]);
const userSignedIn = async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
navigate('loginFlow')
}
};
const signIn = () => {
};
const signUp = () => {
};
const logOut = () => {
};
return (
<AuthContext.Provider value={{ user, userSignedIn, signIn, signUp, logOut }}>
{ children }
</AuthContext.Provider>
)
}
And here is the component which is attempting to use the context:
import React, { useContext, useEffect } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import AuthContext from '../context/AuthContext';
const LoadingScreen = ({ navigation }) => {
const { userSignedIn } = useContext(AuthContext)
useEffect(() => {
userSignedIn()
}, [])
return (
<View style={styles.mainView}>
<ActivityIndicator style={styles.indicator} />
</View>
)
}
And finally, here is my app.js file (cut most of it out due to length, but wanted to show Provider):
import { Provider as AuthProvider } from './src/context/AuthContext';
const App = createAppContainer(switchNavigator)
export default () => {
return (
<AuthProvider>
<App />
</AuthProvider>
)
};
Can anyone see what's going wrong here?
You exported your AuthContext as a named-export ... but you're importing a default-export
import AuthContext from '../context/AuthContext'; // <--- Here
const LoadingScreen = ({ navigation }) => {};
Instead...
import { AuthContext} from '../context/AuthContext';
Same goes for this one as well...
import { Provider as AuthProvider } from './src/context/AuthContext';
Which should be
import { AuthContext: { Provider as AuthProvider } } from './src/context/AuthContext';
OR
import { AuthContext } from './src/context/AuthContext';
return (
<AuthContext.Provider>
<App />
</AuthContext.Provider>
)
I have a simple Dashboard component that relies on React context to manage auth. It contains a custom hook useAuth to extract the current user as well as the auth related functions: login, logout, etc.
This is the Context file: AuthContext.js:
import React, { createContext, useContext, useState, useEffect } from "react";
import { auth } from "../config/firebase";
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password);
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
function logout() {
return auth.signOut();
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
signup,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
This is the Dashboard.js component:
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
export default function Dashboard() {
const { currentUser, logout } = useAuth();
const [error, setError] = useState("");
const history = useHistory();
const handleLogout = async () => {
setError("");
try {
await logout();
history.push("/login");
} catch (e) {
setError(e.message);
}
};
return (
<div>
{error && <p>{error}</p>}
<h1>This is the Dashboard</h1>
<h5>Email: {currentUser.email}</h5>
<button onClick={handleLogout} type="button">
Logout
</button>
</div>
);
}
As recommened by React Testing Library I have created a test-utils.js file:
import React, { createContext } from "react";
import { render } from "#testing-library/react";
import { BrowserRouter as Router } from "react-router-dom";
const AuthContext = createContext();
const currentUser = {
email: "abc#abc.com",
};
const signup = jest.fn();
const login = jest.fn();
const logout = jest.fn();
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};
const customRender = (ui, options) => {
render(ui, { wrapper: AllTheProviders, ...options });
};
export * from "#testing-library/react";
export { customRender as render };
However, when running Dashboard.test.js I get error
TypeError: Cannot destructure property 'currentUser' of '((cov_5mwatn2cf(...).s[0]++) , (0 , _AuthContext.useAuth)(...))' as it is undefined.
4 |
5 | export default function Dashboard() {
> 6 | const { currentUser, logout } = useAuth();
| ^
7 | const [error, setError] = useState("");
8 | const history = useHistory();
import React from "react";
import Dashboard from "./Dashboard";
import { act, render, screen } from "../config/test-utils-dva";
beforeEach(async () => {
await act(async () => {
render(<Dashboard />);
});
});
test("displays dashboard", () => {
expect(screen.getByText(/dashboard/i)).toBeInTheDocument();
});
I think it is because Dashboard component is trying to use useAuth from AuthContext.js, how can I force the rendered Dashboard component to use the mocked data that I am sending in the test-utils.jsfile?
Instead of creating a new context, use the AuthContext from context/AuthContext for <AuthContext.Provider>, as that's the context that the hook uses.
So, in AuthContext.js, export the context instance:
export const AuthContext = createContext();
Then, in your test-util.js file, instead of again calling createContext (which will create a completely separate context instance - the contexts are not the same even if they are stored in a variable with the same name!), just import the previously exported instance:
import { AuthContext } from "../context/AuthContext";
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};