I'm working with authentication in React for the first time and I found it difficult to create it using contexts. When I destructuring an context in my App component, the variables get the value of undefined.
App file (renders the different project routes):
import React, { useContext } from 'react';
/* routes */
import { useRoutes } from 'hookrouter';
import { authRoutes } from './store/routes/auth.routes';
import { appRoutes } from './store/routes/app.routes';
/* contexts */
import { AuthContext } from './contexts/auth';
import { AuthProvider } from './contexts/auth';
function App() {
const { signed, user } = useContext(AuthContext);
const routes = signed ? appRoutes : authRoutes;
// the idea is that when the value of signed was changed (sign in / sign out), this code would be executed again and the routes corresponding to the current value would be rendered
console.log('context s:', signed); // undefined
console.log('context u:', user); // undefined
return (
<AuthProvider>
{ useRoutes(routes) }
</AuthProvider>
);
}
export default App;
AuthContext file:
import React, { createContext, useState } from 'react';
import * as auth from '../services/auth';
const AuthContext = createContext({});
export const AuthProvider = ({children}) => {
const [user, setUser] = useState(null);
async function signIn() {
const response = await auth.signIn();
// simulates an API call
setUser(response.user);
}
function signOut() {
setUser(null);
}
return (
<AuthContext.Provider value={{signed: !!user, user, signIn, signOut}}>
{children}
</AuthContext.Provider>
);
}
export { AuthContext }
Login file - authentication function:
const { signed, signIn } = useContext(AuthContext);
// I don't know why, but in that file the useContext works fine, returning the correct values on destructuring
console.log('signed:', signed); // starts as false, and becomes true when executing the authentication function below
const authenticateUser = (userData) => {
signIn();
}
Basically, I want the App to be re-rendered every time the AuthContext's signed variable changes (ie at sign in and sign out). The problem here is that the AuthContext variables inside the App function take on the value of undefined, but on the login screen they take on the correct value, and I don't see any difference in what I did between the two files. How to solve this?
Related
I set up my React project to use firebase auth using the modular v9 SDK like so. I now would like to create other hooks like useAnalytics, useFirestore, etc, that will allow me to access those utilities in components that need them. But if I use this same pattern for other firebase services, I will end up having to wrap my app with several contexts.
So instead of this auth provider component, I'm thinking of replacing it with a FirebaseProvider component that will wrap everything, but I am not sure if that is correct, or how I would integrate it with the existing auth code below.
import React, {useState, useContext, useEffect, createContext } from "react"
import {
getAuth,
signOut,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
} from "firebase/auth";
// I need to move this elsewhere like index.js
import { initializeApp } from "firebase/app";
firebase.initializeApp(<app config object>);
const auth = getAuth();
const authContext = createContext();
export const useAuth = () => {
return useContext()
}
// my App component is wrapped with this JSX element
export const ProvideAuth = ({children}) => {
const auth = useProvideAuth();
return <authContext.Provider value={auth}></authContext.Provider>
}
const useProvideAuth = () => {
const [user, setUser] = useState(null)
const signIn = (email, password) => {
signInWithEmailAndPassword(auth, email, password).then((res) => {
setUser(res.user)
})
}
const createUser = ...
const signOut = ...;
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
});
// remove listener on unmount
return () => unsubscribe();
}, []);
return {user, signIn, signOut, createUser};
}
I tried placing initializeApp in the index.js, but that code never seems to run and causes the authentication to fail.
I'm learned that React will re-render after state changed e.g. setState from useState(), calling the function or variable from useContext() variable. But now I'm don't understand that why I get the ESLint warning call the context function inside the useCallback() without dependency in the list. If I put the dependency in the list, useCallback() will be re-rendered and useEffect() dependency from useCallback() variable will do again. So how to fix the react-hooks/exhaustive-deps when calling the function inside the useContext() variable?
Auth.js
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import * as AuthAPI from "../API/AuthAPI"
import Loading from "../Page/Loading"
const AuthContext = createContext()
export const AuthProvider = ({children}) => {
const [user,setUser] = useState()
const [loadingInitial,setLoadingInitial] = useState(true)
useEffect(()=>{
AuthAPI.getCurrentUser()
.then((user)=>setUser(user))
.catch((error)=>{console.log(error)})
.finally(()=>setLoadingInitial(false))
},[])
const login = async (email,password) => {
const user = await AuthAPI.login({email,password})
setUser(user)
return user
}
const register = async (firstname,lastname,email,password) => {
const user = await AuthAPI.register({firstname,lastname,email,password})
setUser(user)
return user
}
const logout = async () => {
const response = await AuthAPI.logout()
setUser(undefined)
}
const value = useMemo(()=>({
user,
setUser,
login,
register,
logout
}),[user])
return (
<AuthContext.Provider value={value}>
{loadingInitial ? <Loading/> : children}
</AuthContext.Provider>
)
}
export const useAuth = () => {
return useContext(AuthContext)
}
Logout.js
import { useCallback, useEffect, useState } from "react";
import { Navigate, useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../Hooks/Auth";
import * as AuthAPI from "../API/AuthAPI"
import Loading from "./Loading";
function Logout() {
const auth = useAuth()
const location = useLocation()
const navigate = useNavigate()
const [isLoggedOut,setIsLoggedOut] = useState(false)
const logout = useCallback(async () => {
console.log("Logging out!")
await AuthAPI.logout()
auth.setUser((prevState)=>(undefined))
setIsLoggedOut(true)
},[auth]) // --> re-rendered bacause `auth` context in re-rendered when set `user` state.
useEffect(()=>{
logout()
},[logout]) // --> this also to run again from `logout` callback is being re-rendered.
if (!isLoggedOut) {
return <Loading/>
}
return (
<Navigate to="/login" replace/>
)
}
export default Logout
Any help is appreciated.
How about destructuring your auth context, since you are only using setUser inside useEffect?
const { setUser } = useAuth()
useEffect(() => {
....
}, [setUser])
There is no need for creating a memoized logout callback function if logout isn't used/passed as a callback function. Just apply the logging out logic in the useEffect hook.
Render the Loading component and issue the imperative redirect from the resolved Promise chain of the return AuthAPI.logout Promise.
Example:
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../Hooks/Auth";
import * as AuthAPI from "../API/AuthAPI"
import Loading from "./Loading";
function Logout() {
const auth = useAuth();
const navigate = useNavigate();
useEffect(() => {
console.log("Logging out!");
AuthAPI.logout()
.then(() => auth.setUser(undefined))
.finally(() => navigate("/login", { replace: true }));
}, []);
return <Loading />;
}
export default Logout;
Can you try to replace your useEffect code into this:
useEffect(logout, [])
Can anyone help me with React Hooks basics, I am relatively new and couldn't find proper help online
import React from 'react'
import { auth, provider } from "../../../firebaseSetup";
import { useNavigate } from "react-router-dom"
const GoogleAuth = async() => {
const navigate = useNavigate()
auth.signInWithPopup(provider).then(() => {
navigate('/home');
}).catch((error) => {
console.log(error.message)
})
}
export default GoogleAuth
I get error on const navigate = useNavigate() saying:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component
What they want for useNavigate (and all hooks) is to be called only at the top level of a React component or a custom hook.
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns.
See Rules of Hooks for more.
A solution to your problem could be calling const navigate = useNavigate() in the component where you will use GoogleAuth, and pass navigate as parameter.
As an example like so:
import React from 'react'
import { auth, provider } from "../../../firebaseSetup";
import { useNavigate } from "react-router-dom"
const GoogleAuth = async(navigate) => {
auth.signInWithPopup(provider).then(() => {
navigate('/home');
}).catch((error) => {
console.log(error.message)
})
}
export default GoogleAuth
import GoogleAuth from "GoogleAuth";
const App = ()=>{
/*
here at the top level, not inside an if block,
not inside a function defined here in the component...
*/
const navigate = useNavigate();
useEffect(()=>{
GoogleAuth(navigate)
},[])
return <div></div>
}
export default App;
Please refer to the code below:
Auth.tsx
import { createContext, useEffect, useState, useContext, FC } from 'react';
interface Props {
// any props that come into the component
}
export const PUBLIC_ROUTES = ['/', '/admin/login'];
export const isBrowser = () => typeof window !== 'undefined';
const AuthContext = createContext({
isAuthenticated: false,
isLoading: false,
user: {},
});
export const useAuth = () => useContext(AuthContext);
export const AuthProvider: FC<Props> = ({ children }) => {
const [user, setUser] = useState({});
const [isLoading, setLoading] = useState(true);
useEffect(() => {
async function loadUserFromCookies() {
// const token = Cookies.get('token');
// TODO: Get the token from the cookie
const token = true;
if (token) {
// console.log("Got a token in the cookies, let's see if it is valid");
// api.defaults.headers.Authorization = `Bearer ${token}`;
// const { data: user } = await api.get('users/me');
// if (user) setUser(user);
}
setLoading(false);
}
loadUserFromCookies();
}, []);
return (
<AuthContext.Provider
value={{ isAuthenticated: !!Object.keys(user).length, user, isLoading }}
>
{children}
</AuthContext.Provider>
);
};
export const ProtectRoute: FC<Props> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <div>Loading</div>;
}
if (PUBLIC_ROUTES.includes(window.location.pathname)) {
return <>{children}</>;
}
// If the user is not on the browser or not authenticated
if (!isBrowser() || !isAuthenticated) {
window.location.replace('/login');
return null;
}
return <>{children}</>;
};
_app.tsx
import React from 'react';
import Head from 'next/head';
import { AppProps } from 'next/dist/next-server/lib/router/router';
import { ThemeProvider, StyledEngineProvider } from '#material-ui/core/styles';
import CssBaseline from '#material-ui/core/CssBaseline';
import theme from '../utils/theme';
import { AuthProvider, ProtectRoute } from 'contexts/auth';
export default function MyApp(props: AppProps) {
const { Component, pageProps } = props;
React.useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles && jssStyles.parentElement) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);
return (
<React.Fragment>
<Head>
<title>Next App</title>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<AuthProvider>
<ProtectRoute>
<Component {...pageProps} />
</ProtectRoute>
</AuthProvider>
</ThemeProvider>
</StyledEngineProvider>
</React.Fragment>
);
}
Problems:
So, as per the code, if the user is not logged in, redirect the user
to the login page. But, due to the current logic, 404-page
routes are also redirected to the admin login page. How can I catch
the 404 status to redirect to the 404 pages before verifying if the
user is logged in or not?
I am using an array to verify if the path is public or not. Is there
a better way to render public paths without maintaining hard-coded
page paths or using HOCs? The issue with my approach is if any developer changes the file name and if it doesn't match the string in the array, it will not be a public route. If I create a folder called public and all public pages inside it, I get an unnecessary public/ in my URL. I do not want to use HOCs because I have to import them every time I create a new page which isn't incorrect but I am expecting a better approach.
Thank you.
You can use HOC (Higher Order Components) to wrap the paths which:
can only accessed with authorization;
can only be access when
user unauthenticated (e.g /login -> you don't want user to
access /login page when he's already logged in)
Like so:
login.js -> /login
import withoutAuth from 'path/withoutAuth';
function Login() {
return (<>YOUR COMPONENT</>);
};
export default withoutAuth(Login);
Same can be done with withAuth.js -> HOC (Higher Order Component) which wraps components which can only accessed with authentication
protectedPage.js -> /protectedPage
import withAuth from 'path/withAuth';
function ProtectedPage() {
return (<>YOUR COMPONENT</>);
};
export default withAuth(ProtectedPage);
You can how you can make these HOCs in this article.
Setting up authentication and withConditionalRedirect (HOC which will be used to make withAuth & withoutAuth HOCs): Detecting Authentication Client-Side in Next.js with an HttpOnly Cookie When Using SSR.
Making withAuth & withoutAuth HOCs: Detecting a User's Authenticated State Client-Side in Next.js using an HttpOnly Cookie and Static Optimization
I have a custom hook that will check whether you are logged in, and redirect you to the login page if you are not. Here is a pseudo implementation of my hook that assumes that you are not logged in:
import { useRouter } from 'next/router';
export default function useAuthentication() {
if (!AuthenticationStore.isLoggedIn()) {
const router = useRouter();
router.push('/login');
}
}
But when I use this hook, I get the following error:
Error: No router instance found. you should only use "next/router" inside the client side of your app. https://err.sh/vercel/next.js/no-router-instance
I checked the link in the error, but this is not really helpful because it just tells me to move the push statement to my render function.
I also tried this:
// My functional component
export default function SomeComponent() {
const router = useRouter();
useAuthentication(router);
return <>...</>
}
// My custom hook
export default function useAuthentication(router) {
if (!AuthenticationStore.isLoggedIn()) {
router.push('/login');
}
}
But this just results in the same error.
Is there any way to allow routing outside of React components in Next.js?
The error happens because router.push is getting called on the server during SSR on the page's first load. A possible workaround would be to extend your custom hook to call router.push inside a useEffect's callback, ensuring the action only happens on the client.
import { useEffect } from 'react';
import { useRouter } from 'next/router';
export default function useAuthentication() {
const router = useRouter();
useEffect(() => {
if (!AuthenticationStore.isLoggedIn()) {
router.push('/login');
}
}, []);
}
Then use it in your component:
import useAuthentication from '../hooks/use-authentication' // Replace with your path to the hook
export default function SomeComponent() {
useAuthentication();
return <>...</>;
}
import Router from 'next/router'
create a HOC which will wrap your page component
import React, { useEffect } from "react";
import {useRouter} from 'next/router';
export default function UseAuthentication() {
return () => {
const router = useRouter();
useEffect(() => {
if (!AuthenticationStore.isLoggedIn()) router.push("/login");
}, []);
// yous should also add isLoggedIn in array of dependancy if the value is not a function
return <Component {...arguments} />;
};
}
main component
function SomeComponent() {
return <>...</>
}
export default UseAuthentication(SomeComponent)