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>
);
};
Related
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.
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 have an app using ReactJS and Parse Server. But the data that I'm passing on from the API is not setting after refreshing the page. Here's my code
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import Parse from 'parse';
const AuthContext = React.createContext({});
const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoggingIn, setLoginLoading] = useState(null);
const [loginError, setLoginError] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(Parse.User.current());
const setUserSession = currentUser => {
setIsAuthenticated(currentUser);
setLoginLoading(false);
setLoginError(null);
};
const handleLogin = async values => {
setLoginLoading(true);
setLoginError(null);
setSessionError(false);
try {
const res = await Parse.Cloud.run('login', values);
setUser(res); // THIS IS THE DATA I WANT TO GET FROM THE API
Parse.User.become(res.session).then(setUserSession, () => {
setLoginError(TOKEN_VERIFY_ERROR);
setLoginLoading(false);
});
setLoginLoading(false);
} catch (e) {
setLoginLoading(false);
setLoginError(e.message);
}
};
};
return (
<AuthContext.Provider
value={{
user,
login: handleLogin,
isLoggingIn,
isAuthenticated,
loginError,
}}
>
{children}
</AuthContext.Provider>
);
const AuthProvider = withRouter(AuthContextProvider);
export { AuthContext, AuthProvider };
AuthContextProvider.propTypes = {
children: PropTypes.node,
};
upon logging in, user are there but when I refresh it, it returns null. What should I do to retain the details? I'm avoiding setting this to localStorage.
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>
)