isAuthenticated is always null - reactjs

isAuthenticated is always null
I am trying to compare token expired or not but isAuthenticated is always null
import React, {useEffect, useState} from 'react';
import {Navigate} from 'react-router-dom'
import jwtDecode from "jwt-decode";
interface IPrivateRoute {
children: React.ReactNode;
}
const PrivateRoute = ({children}: IPrivateRoute) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null)
useEffect(() => {
let token = localStorage.getItem('token')
if (token) {
let decodedToken:number = jwtDecode<any>(token).exp;
let date:number = new Date().getTime() / 1000;
console.log(decodedToken <= date)
if (decodedToken <= date) {
setIsAuthenticated(true)
} else {
setIsAuthenticated(false)
}
} else {
setIsAuthenticated(false)
}
})
return (
<>
{isAuthenticated ? children : <Navigate to='/login'/>}
</>
);
};
export default PrivateRoute;

I think the issue is that isAuthenticated IS null on the initial render, which is a falsey value, and the navigation to "/login" occurs, unmounting this PrivateRoute wrapper component. The useEffect never gets to update the state to anything meaningful while the component is still mounted.
You've likely a couple options:
Conditionally render nothing (or loading indicator, etc...) while the isAuthenticated state is settled then conditionally render children or Navigate.
const PrivateRoute = ({ children }: IPrivateRoute) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
useEffect(() => {
const token = JSON.parse(localStorage.getItem('token'));
if (token) {
const decodedToken: number = jwtDecode<any>(token).exp;
const date: number = new Date().getTime() / 1000;
setIsAuthenticated(decodedToken <= date);
} else {
setIsAuthenticated(false);
}
});
if (isAuthenticated === null) return null;
return isAuthenticated ? children : <Navigate to='/login' replace />;
};
Provide a state initializer function to provide the valid boolean true|false isAuthenticated value you are looking for on the initial render.
const initializeState = () => {
const token = JSON.parse(localStorage.getItem('token'));
if (token) {
const decodedToken: number = jwtDecode<any>(token).exp;
const date: number = new Date().getTime() / 1000;
return decodedToken <= date;
}
return false;
};
...
const PrivateRoute = ({ children }: IPrivateRoute) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(initializeState);
useEffect(() => {
const token = JSON.parse(localStorage.getItem('token'));
if (token) {
const decodedToken: number = jwtDecode<any>(token).exp;
const date: number = new Date().getTime() / 1000;
setIsAuthenticated(decodedToken <= date);
} else {
setIsAuthenticated(false);
}
});
return isAuthenticated ? children : <Navigate to='/login' replace />;
};

Related

AuthContext user is null on page refresh in Nextjs nested layout

I have following bit of code where <AuthContext> provide authentication objects and methods, <ProtectedRoute> component which guards the authentication required routes.
But the problem is when I login and refresh inside authenticated page, user object fetched from the useAuth hook returns null, but if I use next/link it works fine and user object is preserved.
AuthContext.tsx
const AuthContext = createContext<any>({})
export const useAuth = () => useContext(AuthContext)
type Props = {
children: React.ReactNode
}
const LOGIN = gql`...`
export const AuthContextProvider = ({ children }: Props) => {
const [user, setUser] = useState<object | null>(null)
const [loginUser, { data, loading, error }] = useMutation(LOGIN)
const router = useRouter()
useEffect(() => {
// in case of first login set user and token
// push user to route he belongs
if (data != null) {
if (data.authenticate.jwt !== null) {
console.log('Setting user with JWT decoding...')
const decoded = jwt_decode(data.authenticate.jwt)
setUser({
role: decoded.role,
id: decoded.id,
})
localStorage.setItem('token', data.authenticate.jwt)
}
}
const token = localStorage.getItem('token')
// if token is present set the user object
if (token !== null) {
console.log('Token present')
const decoded = jwt_decode(token)
// console.log(user)
console.log(`Decoded token : ${JSON.stringify(decoded)}`)
setUser({
role: decoded.role,
id: decoded.id,
})
} else {
setUser(null)
}
}, [data])
const login = async (username: string, password: string) => {
loginUser({
variables: {
username,
password,
},
})
}
const logOut = async () => {
console.log('Logging out ...')
setUser(null)
data = null
localStorage.removeItem('token')
}
// if (error) return <p>Submission error! ${error.message}</p>
return (
<AuthContext.Provider value={{ user, login, logOut }}>
{error || loading ? null : children}
</AuthContext.Provider>
)
}
ProtectedRoute.tsx
const PUBLIC_PATHS = ['/']
const ADMIN_ROUTES = [...]
const SUPERVISOR_ROUTES = [...]
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user } = useAuth()
const router = useRouter()
const [authorized, setAuthorized] = useState(false)
useEffect(() => {
console.log(`Current User : ${JSON.stringify(user)}`)
const isAdminRoute = ADMIN_ROUTES.includes(router.pathname)
const isSupervisorRoute = SUPERVISOR_ROUTES.includes(router.pathname)
// if an token is present send user to
// authorized route
if (user !== null) {
// #ts-ignore
if (user.role === 1 && isAdminRoute) {
setAuthorized(true)
console.log(`Pushing to ${router.pathname}`)
router.push(router.pathname)
// #ts-ignore
} else if (user.role === 2 && isSupervisorRoute) {
setAuthorized(true)
console.log(`Pushing to ${router.pathname}`)
router.push(router.pathname)
} else {
console.log(`Invalid role! user: ${JSON.stringify(user)}`)
}
} else {
setAuthorized(false)
console.log('Sending you to login page')
// "/" have the login page
router.push('/')
}
}, [user]) // router is avoided from deps to avoid infinite loop
return <>{authorized && children}</>
}
_app.tsx
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
const noAuthRequired = ['/']
function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page)
const router = useRouter()
return (
<ApolloProvider client={CLIENT}>
<AuthContextProvider>
{getLayout(
noAuthRequired.includes(router.pathname) ? (
<Component {...pageProps} />
) : (
<ProtectedRoute>
<Component {...pageProps} />
</ProtectedRoute>
),
)}
</AuthContextProvider>
</ApolloProvider>
)
}
export default App
Current possible workaround is to read JWT in the ProtectedRoute to get user information.
I there anyway to preserve user object on page refresh?

when i console.log(function) one page to another page its gives me undefined value on the other page but on the real page it gives me real value

when i console.log(function) one page to another page its gives me undefined value on the other page but on the real page it gives me real value. how to solve it in nextjs
function MyApp({ Component, pageProps}) {
const [cart, setCart] = useState({})
const [subTotal, setSubTotal] = useState(0)
useEffect(() => {
console.log("hey i am a use effect from app .js")
try {
if(localStorage.getItem("cart")){
setCart(JSON.parse(localStorage.getItem("cart")))
}
} catch (error) {
console.error(error);
localStorage.clear()
}
}, [])
const saveCart = (myCart) => {
localStorage.setItem("cart",myCart)
let subt = 0;
let keys = Object.keys(cart)
for(let i = 0; i<keys.lenght; i++){
subt += myCart[keys[i]].price * myCart[keys[i]].qty;
}
setSubTotal(subt)
}
const addToCart = (itemCode, qty, price,name,size,variant) => {
let newCart = cart;
if(itemCode in cart){
newCart[itemCode].qty = cart[itemCode].qty + qty;
} else {
newCart[itemCode] = {qty:1,price,name,size,variant}
}
setCart(newCart)
saveCart(newCart)
}
const clearCart = () => {
setCart({})
saveCart({})
}
const removeFromCart = (itemCode, qty, price,name,size,variant) => {
let newCart = cart;
if(itemCode in cart){
newCart[itemCode].qty = cart[itemCode].qty - qty;
}
if(newCart[itemCode]["qty"]<=0){
delete newCart[itemCode]
}
setCart(newCart)
saveCart(newCart)
}
useEffect(()=>{
const mode = localStorage.getItem("mode");
if(mode){
document.documentElement.classList.add("dark");
}
},[])
return (
<Layout {...pageProps} cart={cart} saveCart={saveCart} addToCart={addToCart} clearCart={clearCart} removeFromCart={removeFromCart} subTotal={subTotal}>
<Component {...pageProps} />
</Layout>
);
}
export default MyApp
this is the main page
const NavBarTop2 = ({cart,saveCart,addToCart,clearCart,removeFromCart,subTotal}) => {
console.log(cart,saveCart,addToCart,clearCart,removeFromCart,subTotal)
return (
<div className="nav_top2">
</div>
)
}
export default NavBarTop2;
this is an other page where I want to use them but its gives me undefined variables if anyone knows please help me out this problem
For something like this you should create a react context provider. Using a context provider also allows you to access these functions in any component that is wrapped inside the provider without unnecessary prop drilling. https://reactjs.org/docs/context.html
This is due to how props work for page components in NextJS. You can read more about how NextJS page props work here: https://nextjs.org/docs/basic-features/pages
// CartProvider.jsx
import { createContext, useMemo, useState, useContext } from "react"
const CartContext = createContext(null)
export const useCart = () => useContext(CartContext)
const CartProvider = ({ children }) => {
const [cart, setCart] = useState({})
const [subTotal, setSubTotal] = useState(0)
const saveCart = (myCart) => {
localStorage.setItem("cart",myCart)
let subt = 0;
let keys = Object.keys(cart)
for(let i = 0; i<keys.lenght; i++){
subt += myCart[keys[i]].price * myCart[keys[i]].qty;
}
setSubTotal(subt)
}
const addToCart = (itemCode, qty, price,name,size,variant) => {
let newCart = cart;
if(itemCode in cart){
newCart[itemCode].qty = cart[itemCode].qty + qty;
} else {
newCart[itemCode] = {qty:1,price,name,size,variant}
}
setCart(newCart)
saveCart(newCart)
}
const clearCart = () => {
setCart({})
saveCart({})
}
const removeFromCart = (itemCode, qty, price,name,size,variant) => {
let newCart = cart;
if(itemCode in cart){
newCart[itemCode].qty = cart[itemCode].qty - qty;
}
if(newCart[itemCode]["qty"]<=0){
delete newCart[itemCode]
}
setCart(newCart)
saveCart(newCart)
}
const providerValue = useMemo(() => ({
cart,
saveCart,
addToCart,
clearCart,
removeFromCart,
subTotal,
}),[])
return (
<CartContext.Provider value={providerValue}>
{ children }
</CartContext.Provider>
)
}
export default CartProvider
// _app.jsx
import CartProvider from './CartProvider.jsx'
export default function MyApp({ Component, pageProps }) {
return (
<CartProvider>
<Component {...pageProps} />
</CartProvider>
)}
// NavBarTop2.jsx
import { useCart } from './CartProvider'
const NavBarTop2 = () => {
const {
cart,
saveCart,
addToCart,
clearCart,
removeFromCart,
subTotal,
} = useCart()
return ( //return here )
}
Controll from where you are using "NavBarTop2", the undefined in the console.log means that you are not passing anything to NavBarTop2.
Is not that the functions defined in your main are than automatically available.

useContext not reading values on first refresh

export const LoginContext = React.createContext();
export const DetailsContext = React.createContext();
function App() {
const username = localStorage.getItem("bankDetails");
const [userDetails, setUserDetails] = useState({});
const [isValid, setisValid] = useState(false);
useEffect(() => {
if (username !== null) {
Axios.post("http://localhost:3001/userDetails", {
username: username,
}).then((res) => {
if (res.data.err) {
console.log("err");
} else {
setUserDetails(res.data.details[0]);
setisValid(true);
}
});
}
}, []);
return (
<LoginContext.Provider value={{ isValid, setisValid }}>
<DetailsContext.Provider value={{ userDetails, setUserDetails }}>
<Router>
<Routes>
<Route ... />
</Routes>
</Router>
</DetailsContext.Provider>
</LoginContext.Provider>
);
}
export default App;
Transactions.js
function Transactions() {
const { isValid } = useContext(LoginContext);
const { userDetails, setUserDetails } = useContext(DetailsContext);
const [allDetails, setAllDetails] = useState([]);
const [transactions, setTransactions] = useState([]);
useEffect(() => {
console.log(userDetails);
Axios.get("http://localhost:3001/transactTo").then((rest) => {
setAllDetails(rest.data);
});
// setTransactions(JSON.parse(userDetails.transactions));
}, [userDetails]);
return isValid ? <h1>Valid</h1> : <h1>Not Valid</h1>
}
export default Transactions;
The userDetails logs an empty object first and data object after re-render but after uncommenting the setTransactions(JSON.parse(userDetails.transactions)) part it only logs an empty object and then an error stating: Unexpected token u in JSON at position 0. It only happens on page refresh and not when I navigate from another page.
Also tried adding second effect but it didn't helped:
useEffect(() => {
setTransactions(JSON.parse(userDetails.transactions));
}, [allDetails]);
It is an empty object because API requests are asynchronous. It is a normal thing.
Unexpected token u in JSON at position 0 this means that userDetails.transactions isn't a json. It's probably undefined that's why u
useEffect(() => {
// try returning when the property is `undefined`
if (!userDetails.transations) return;
setTransactions(JSON.parse(userDetails.transactions));
}, [allDetails]);

Custom hook in React JS : arguments not updating after rerender

I have a date range filter in which when submitted, it will update new values of dateStart and dateEnd using setDateStart and setDateEnd then it will pass down the new values to my useCollection custom hook. Why is it that when it rerenders, the useCollection custom hook arguments doesn't get the updated values?
*the custom useCollection hook is used for data Fetching on Firebase
*the dateStart and dateEnd is used for filtering the data displayed on the screen
useCollection custom hook:
import { useEffect, useState, useRef } from "react";
import { projectFirestore } from "../firebase/config";
export const useCollection = (
collection,
_query,
_query2,
_query3,
_orderBy
) => {
const [documents, setDocuments] = useState(null);
const [error, setError] = useState(null);
// if we don't use a ref --> infinite loop in useEffect
// _query is an array and is "different" on every function call
const query = useRef(_query).current;
const query2 = useRef(_query2).current;
const query3 = useRef(_query3).current;
const orderBy = useRef(_orderBy).current;
console.log("from query: " + query);
console.log("from query2: " + query2);
console.log("from query2: " + query3);
useEffect(() => {
let ref = projectFirestore.collection(collection);
if (query) {
ref = ref.where(...query);
}
if (query2) {
ref = ref.where(...query2);
}
if (query3) {
ref = ref.where(...query3);
}
if (orderBy) {
ref = ref.orderBy(...orderBy);
}
const unsubscribe = ref.onSnapshot(
(snapshot) => {
let results = [];
snapshot.docs.forEach((doc) => {
results.push({ ...doc.data(), id: doc.id });
});
// update state
setDocuments(results);
setError(null);
},
(error) => {
console.log(error);
setError("could not fetch the data");
}
);
// unsubscribe on unmount
return () => unsubscribe();
}, [collection, query, query2, query3, orderBy]);
return { documents, error };
};
TimeCard.js
import React, { useState, useEffect } from "react";
import moment from "moment";
import { useAuthContext } from "../../hooks/useAuthContext";
import { useCollection } from "../../hooks/useCollection";
import Table from "../../components/Table";
import DateRange from "../../components/DateRange";
import ErrorMessage from "../../components/ErrorMessage";
const TimeCard = () => {
const { uid } = useAuthContext().user;
let m1 = moment(new Date());
let m2 = moment();
m1.startOf("month").startOf("day");
m2.endOf("day");
const [dateStart, setDateStart] = useState(m1.toDate());
const [dateEnd, setDateEnd] = useState(m2.toDate());
console.log("Moment1 from State: ", dateStart);
console.log("Moment2: from State", dateEnd);
const [time1, setTime1] = useState("");
const [time2, setTime2] = useState("");
const [error, setError] = useState("");
console.log("RENDER");
const { documents } = useCollection(
"timeinout",
["uid", "==", uid],
["createdAt", ">=", dateStart],
["createdAt", "<=", dateEnd],
["createdAt", "asc"]
);
const handleSubmit = (e) => {
e.preventDefault();
var d1 = new Date(time1);
var d2 = new Date(time2);
if (!time1 || !time2) {
setError(
"Either ONE or BOTH of the date inputs below are empty, please select a date!"
);
return null;
}
if (d1.getTime() > d2.getTime()) {
setError(
"Invalid Date: Date 2 (To:) Input is greater than Date 1 (From:) Input"
);
return null;
}
const s1 = moment(time1).startOf("day").toDate();
const s2 = moment(time2).endOf("day").toDate();
setDateStart(s1);
setDateEnd(s2);
};
useEffect(() => {
const time = setTimeout(() => {
setError("");
}, [5000]);
return () => {
clearTimeout(time);
};
}, [error]);
return (
<>
<div className="flex flex-col">
{error && <ErrorMessage msg={error} />}
<DateRange
time1={time1}
time2={time2}
setTime1={setTime1}
setTime2={setTime2}
handleSubmit={handleSubmit}
/>
</div>
{documents && <Table dataFromDatabase={documents} />}
</>
);
};
export default TimeCard;
Implemented it without using a custom hook instead, basically all the codes from the useCollection custom hook were transferred to TimeCard.js and created a normal useEffect hook that process all the changes in the filter for the dates.
See code below:
import React, { useState, useEffect } from "react";
import { projectFirestore } from "../../firebase/config";
import moment from "moment";
import { useAuthContext } from "../../hooks/useAuthContext";
import Table from "../../components/Table";
import DateRange from "../../components/DateRange";
import ErrorMessage from "../../components/ErrorMessage";
const TimeCard = () => {
const { uid } = useAuthContext().user;
const [error, setError] = useState("");
const [documents, setDocuments] = useState(null);
const startOfMonth = moment().startOf("month").format("YYYY-MM-DD");
const endOfMonth = moment().endOf("month").format("YYYY-MM-DD");
console.log("startOfMonth: ", startOfMonth);
console.log("endOfMonth: ", endOfMonth);
const [time1, setTime1] = useState(startOfMonth);
const [time2, setTime2] = useState(endOfMonth);
const [query] = useState(["uid", "==", uid]);
const [query2, setQuery2] = useState([
"createdAt",
">=",
moment(time1).startOf("day").toDate(),
]);
const [query3, setQuery3] = useState([
"createdAt",
"<=",
moment(time2).endOf("day").toDate(),
]);
const [orderBy] = useState(["createdAt", "asc"]);
const handleSubmit = (e) => {
e.preventDefault();
var d1 = new Date(time1);
var d2 = new Date(time2);
if (!time1 || !time2) {
setError(
"Either ONE or BOTH of the date inputs below are empty, please select a date!"
);
return null;
}
// Loseless code below because of > comparison operator
if (d1.getTime() > d2.getTime()) {
setError(
"Invalid Date: Date 2 (To:) Input is greater than Date 1 (From:) Input"
);
return null;
}
setQuery2(["createdAt", ">=", moment(time1).startOf("day").toDate()]);
setQuery3(["createdAt", "<=", moment(time2).endOf("day").toDate()]);
};
useEffect(() => {
const time = setTimeout(() => {
setError("");
}, [5000]);
return () => {
clearTimeout(time);
};
}, [error]);
useEffect(() => {
let ref = projectFirestore.collection("timeinout");
if (query) {
ref = ref.where(...query);
}
if (query2) {
ref = ref.where(...query2);
}
if (query3) {
ref = ref.where(...query3);
}
if (orderBy) {
ref = ref.orderBy(...orderBy);
}
const unsubscribe = ref.onSnapshot(
(snapshot) => {
let results = [];
snapshot.docs.forEach((doc) => {
results.push({ ...doc.data(), id: doc.id });
});
// update state
setDocuments(results);
setError(null);
},
(error) => {
console.log(error);
setError("could not fetch the data");
}
);
// unsubscribe on unmount
return () => unsubscribe();
}, [query, query2, query3, orderBy]);
console.log("Updated documents OUTSIDE: ", documents);
return (
<>
<div className="flex flex-col">
{error && <ErrorMessage msg={error} />}
<DateRange
time1={time1}
time2={time2}
setTime1={setTime1}
setTime2={setTime2}
handleSubmit={handleSubmit}
/>
</div>
{documents && <Table documents={documents} />}
</>
);
};
export default TimeCard;

Best way to check if there is already a token in local storage using use effect in React Context

good day. Is this the best way to check if there is already a token in my local storage in my AuthContext? I used a useEffect hook in checking if the token already exists. Should I change the initial state of isAuthenticated since it is always false upon rendering? Im not sure. Thank you
import React, { useState, useContext, useEffect } from "react";
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setAuthenticated] = useState(false);
const login = () => {
setAuthenticated(true);
};
useEffect(() => {
const token = localStorage.getItem("AuthToken");
if (token) {
setAuthenticated(true);
} else if (token === null) {
setAuthenticated(false);
}
return () => {};
}, []);
return <AuthContext.Provider value={{ isAuthenticated, login }}>{children}</AuthContext.Provider>;
};
I would suggest using a state initializer function so you have correct initial state. You won't need to wait until a subsequent render cycle to have the correct authentication state.
const [isAuthenticated, setAuthenticated] = useState(() => {
const token = localStorage.getItem("AuthToken");
return token !== null;
});
The rest can likely remain the same.
I suggest you also provide a default context value as well.
const AuthContext = React.createContext({
isAuthenticated: false,
login: () => {},
});
export function useAuth() {
return useContext(AuthContext);
}
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setAuthenticated] = useState(() => {
const token = localStorage.getItem("AuthToken");
return token !== null;
});
const login = () => setAuthenticated(true);
return (
<AuthContext.Provider value={{ isAuthenticated, login }}>
{children}
</AuthContext.Provider>
);
};

Resources