AuthContext user is null on page refresh in Nextjs nested layout - reactjs

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?

Related

how avoid 401 error everytime with axios?

I have a project with react and an java spring boot api. For connect to the api, after SignIn with google login, i do this :
import axios from 'axios'
import { useNavigate } from 'react-router-dom';
import { signInRoute } from '../../routing';
export function SignInGetService() {
axios.get(process.env.REACT_APP_API_SERVER_URL + '/url', {})
.then(response => { });
}
export async function SignInPostService(token) {
return await axios.post(process.env.REACT_APP_API_SERVER_URL + '/mySignInUrl', { "token": token })
.then(response => {
axios.defaults.headers.common['X-AUTH-TOKEN'] = response.data['data'];
return response.data['data']
}).catch(function(error) {
if(error.response.status == 500){
window.localStorage.removeItem('userFromStorage');
window.location.reload()
}
});
}
in app.tsx my code is :
interface AuthContextType {
user: any;
signin: (userMail: string, callback: VoidFunction, userObject: Object) => void;
signout: (callback: VoidFunction) => void;
}
let AuthContext = React.createContext<AuthContextType>(null!);
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<any>(window.localStorage.getItem('userFromStorage'));
let signin = (newUserMail: string, callback: VoidFunction, newUserObject: Object) => {
return fakeAuthProvider.signin(() => {
setUser(JSON.stringify(newUserMail));
window.localStorage.setItem('userFromStorage', JSON.stringify(newUserMail));
callback();
});
};
let signout = (callback: VoidFunction) => {
return fakeAuthProvider.signout(() => {
setUser(null);
window.localStorage.removeItem('userFromStorage');
callback();
});
};
const value = { user, signin, signout };
const [isLoading, setIsLoading] = useState(true)
if (user != null) {
let r = SignInPostService(JSON.parse(user).token).then(() => {
setIsLoading(false)
})
}
if (user != null && isLoading == false) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
if (user == null) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
}
export function useAuth() {
return React.useContext(AuthContext);
}
function RequireAuth({ children }: { children: JSX.Element }) {
let auth = useAuth();
let location = useLocation();
if (!auth.user) {
return <Navigate to="/SignIn" state={{ from: location }} replace />;
}
return children;
}
So after this, normally everytime I do a request my X-AUTH-TOKEN is suppose to be well set.
And actually it's work. But sometimes, not often but enough for being annoying, I have an 401 error.
And If I refresh the 401 error disappear.
Do you know where it can come from ?
Thank you all.
I expect that the X-AUTH-TOKEN is set everytime.

UseContext returns null in getServer side props NextJS

I have used the context in other places, such as login, database functions, and more. However, when I try to run functions or variables inside my context in places such as custom api's or getServerSideProps, it returns the following error, TypeError: Cannot read properties of null (reading 'useContext'). I am attaching my auth context, my initialization of the context, and the getServerSideProps function that is returning an error
_app.js
import RootLayout from '../components/Layout'
import { AuthProvider } from '../configs/auth-context'
import '../styles/globals.css'
export default function App({ Component, pageProps }) {
return (
<AuthProvider >
<RootLayout>
<Component {...pageProps} />
</RootLayout>
</AuthProvider>
)}
auth-context
import React, { useContext, useState, useEffect, useRef } from 'react'
import { auth, db, provider } from './firebase-config'
import { GoogleAuthProvider, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, onAuthStateChanged, signInWithPopup } from 'firebase/auth'
import { doc, getDoc, setDoc } from 'firebase/firestore'
import {useRouter} from 'next/router';
const AuthContext = React.createContext({currentUser: {uid: "TestUid", email:"Testeremail#email.com"}})
export function UseAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const router = useRouter();
const [currentUser, setCurrentUser] = useState({uid: "TestUid", email:"Testeremail#email.com"})
const [loading, setLoading] = useState(true)
async function signup(email, password) {
createUserWithEmailAndPassword(auth, email, password)
.then(async (result) => {
const user = result.user;
await userToDb(user);
router.push('/portfolio');
return user;
}).catch((error) => {
console.error(error);
})
return
}
async function login(email, password) {
return signInWithEmailAndPassword(auth, email, password)
.then(async (result) => {
const user = result.user;
await userToDb(user);
router.push('/portfolio');
return user;
}).catch((error) => {
console.error(error)
})
}
function logout() {
router.push('/')
return signOut(auth)
}
async function googleSignIn() {
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider)
.then(async (result) => {
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
// The signed-in user info.
const user = result.user;
await userToDb(user);
router.push('/portfolio');
return user
}).catch((error) => {
console.log(error)
// const errorCode = error.code;
// const errorMessage = error.message;
// The email of the user's account used.
// const email = error.customData.email;
// The AuthCredential type that was used.
// const credential = GoogleAuthProvider.credentialFromError(error);
} )
}
const userToDb = async (user) => {
// await setDoc(doc(db, "users", user.uid), {
// userEmail: user.email,
// userID: user.uid
// }, {merge: false})
let currentRef = doc(db, 'users', user.uid)
let currentUserID = user.uid;
let currentEmail = user.email;
await setDoc(currentRef, {
userEmail: currentEmail,
userID: currentUserID
}, {merge: false})
}
function fixData(docs) {
console.log("this works")
// setDocuments(docs);
let retMap = new Map();
if (currentUser !== null) {
docs?.map(function(doc) {
console.log(doc)
let tic = doc.stockTicker
let data = {
shares: doc.shares,
price: doc.price,
type: doc.type
}
if(!retMap.has(tic)) {
retMap.set(tic, [data]);
console.log(tic + " " + data)
// setMap(new Map(datamap.set(tic, {shares: shares, averagePrice: price})))
}
else {
let x = retMap.get(tic);
x.push(data);
}
})
console.log(retMap)
return retMap;
}
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async user => {
setCurrentUser(user)
setLoading(false)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
signup,
logout,
googleSignIn,
fixData
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
getServerSideProps
export async function getServerSideProps() {
let allDocs = []
let avgDocs = []
const {currentUser} = UseAuth()
return {
props: {allDocs, avgDocs}
}
}
I don't know the correct answer, but hooks should be used in components and hooks without exception to ssr.

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]);

isAuthenticated is always null

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 />;
};

React private route function is called twice

If user is logged in, render the component. If not, render login page. I notice, however, that this function is called twice. The first time, useAuthDataContext() is null. The second time, I get the correct object back.
const PrivateRoute = ({ component, ...options }) => {
const { userData } = useAuthDataContext()
console.log(userData)
const finalComponent = userData != null ? component : Login
return (
<Route {...options} component={finalComponent} />
)
};
export default PrivateRoute
I have rewritten this function as follows. Here, PrivateRoute2 is called only once, and useAuthDataContext() returns null.
const PrivateRoute2 = ({ component: Component, ...rest }) => {
const { userData } = useAuthDataContext()
console.log(userData)
return (
<Route
{...rest}
render={props =>
userData != null ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
)
}
Here is my useAuthDataContext() implementation that is causing the rerender:
export const AuthDataContext = createContext(null)
const initialAuthData = {}
const AuthDataProvider = props => {
const [authData, setAuthData] = useState(initialAuthData)
useLayoutEffect( (props) => {
const getUser = async () => {
try {
const userData = await authService.isAuthenticated()
setAuthData( {userData})
} catch (err) {
setAuthData({})
}
}
getUser()
}, [])
const onLogout = () => {
setAuthData(initialAuthData)
}
const onLogin = newAuthData => {
const userData = newAuthData
setAuthData( {userData} )
}
const authDataValue = useMemo(() => ({ ...authData, onLogin, onLogout }), [authData])
return <AuthDataContext.Provider value={authDataValue} {...props} />
}
export const useAuthDataContext = () => useContext(AuthDataContext)
export default AuthDataProvider
I think i found one solution. See this post https://hackernoon.com/whats-the-right-way-to-fetch-data-in-react-hooks-a-deep-dive-2jc13230

Resources