Keep state in react inside context after logged in? - javascript

I'm coding a web react app with sign in. In my server side I'm using express, jwt and sending a httpOnly cookie with the token when succesfully log in. When user logs in, I'm trying to keep state in the client (e.x, loggedIn = true) inside of a context, but every time that context is rendered it comes back to default state (undefined). How could i keep that state in memory?
My user route, that works as expected (backend):
users.post('/login',async (req, res) => {
try {
const {userName,userPass} = req.body
const u = await models.User.findOne({
userid: userName
})
if (!u) res.status(404).end()
if (bcrypt.compare(userPass,u.password)) {
// JWT TOKEN
const t = c_auth(u._id)
res.status(200).cookie("session",t,{
httpOnly:true
}).end()
} else {
res.status(404).end()
}
} catch (e) {
console.log({'ERROR':e})
res.status(500).end()
}
})
My user provider that returns true when request is ok (client):
get: async (user,pass) => {
try {
const req = await axios.post('/users/login',{
userName: user,
userPass: pass
})
if (req.status === 200) {
return true
} else {
return false
}
} catch (e) {
console.log({'ERROR':e})
return false
}
}
Login submit function (client):
import {useAuth} from '../../../../contexts/AuthContext.js'
const {setLoggedIn} = useAuth()
const handleLogin = async (e) => {
if (await users.get(data.userName,data.userPass)) {
setLoggedIn(true)
// ^---> Trying to set loggedIn state to true in context
window.location.replace('/')
} else {
alert(`Incorrect.`)
}
}
Auth context:
const AuthContext = createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider ({children}) {
const [loggedIn,setLoggedIn] = useState()
console.log(loggedIn)
// ^---> Getting true after login,
// undefined (default useState) after re-render
const value = {
loggedIn,
setLoggedIn
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
App.js:
import {AuthProvider} from './contexts/AuthContext'
function App() {
return (
<AuthProvider>
<div className="App">
<Navigation/>
<main>
<Router>
<Routes>
...Routes
</Routes>
</Router>
</main>
</div>
</AuthProvider>
)
}
I suppose that react memo could be the solution, but I don't understand quite well how it works. Also, is it correct not to use the setLoggedIn in the AuthContext itself? I tried to call the login or sign up function (second piece of code) from the AuthContext, but can't set state since its unmounted. Would need to do that inside a useEffect and that's not what I'm looking for since I wouldn`t be able to export that function. All help is appreciated.
EDIT: fixed
The problem solved after changing window.location.replace to the useNavigate hook from react-router-dom, causing a refresh:
import {useAuth} from '../../../../contexts/AuthContext.js'
import {useNavigate} from 'react-router-dom'
const {setLoggedIn} = useAuth()
const navigate = useNavigate()
const handleLogin = async (e) => {
if (await users.get(data.userName,data.userPass)) {
setLoggedIn(true)
navigate('/')
} else {
alert(`Incorrect.`)
}
}
Also in my navbar I was using <a href> tags instead of <Link to> from 'react-router-dom'. That fixes the problem when I go to a different page from the navbar, so it doesn't 'refresh'.

Related

Prevent Router from Loading Page Briefly before Redirect

I have a session context for my NextJS application where anyone accessing /app/ directory pages have to go through an authorization check prior to allowing the user to access the page.
While my logic works in redirecting users without proper authentication, it is a bit glitchy because when someone navigate to the URL, /app/profile/ the page briefly loads before being redirected by Router.
I am wondering what is the best way to have this check happen prior to router loading the unauthorized page and redirecting them to the /login/ page.
Here are the steps in the authorization check:
Check is the user object has a property, authorized
Query the server for a session token
if the object from the server request comes back with authorized = false, then redirect user to /login/
Here is the code:
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import axios from 'axios'
export const SessionContext = createContext(null);
const AppSession = ({ children }) => {
const router = useRouter()
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''})
useEffect(()=> {
// Check for populated user state if pages are accessed with the path /app/
if (routerPath.includes("/app/")){
if (user){
if(user.authenticated === undefined){
// Check if user session exists
axios.get('/api/auth/session/')
.then(res => {
const data = res.data;
// Update user state depending on the data returned
setUser(data)
// If user session does not exist, redirect to /login/
if (data.authenticated === false){
router.push('/login/')
}
})
.catch(err => {
console.log(err)
});
}
}
}
}, [])
return (
<SessionContext.Provider value={{user, setUser}}>
{children}
</SessionContext.Provider>
)
}
export const getUserState = () => {
const { user } = useContext(SessionContext)
return user;
}
export const updateUserState = () => {
const { setUser } = useContext(SessionContext)
return (user) => {
setUser(user);
}
}
export default AppSession;
Since user.authenticated isn't defined in the initial user state you can conditionally render null or some loading indicator while user.authenticated is undefined. Once user.authenticated is defined the code should either redirect to "/login" or render the SessionContext.Provider component.
Example:
const AppSession = ({ children }) => {
const router = useRouter();
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''});
...
if (user.authenticated === undefined) {
return null; // or loading indicator/spinner/etc
}
return (
<SessionContext.Provider value={{ user, setUser }}>
{children}
</SessionContext.Provider>
);
};
Check out getServerSideProps, redirects in getServerSideProps and this article.
In your client-side, if you export the NextJS function definition named getServerSideProps from a page, NextJS pre-renders the page on each request using the data returned by getServerSideProps.
In other words, you can use getServerSideProps to retrieve and check the user while pre-rendering the page and then choose to redirect instead of render if your condition is not met.
Here is an example.
function Page({ data }) {
// Render data...
}
export async function getServerSideProps(context) {
const { req, res } = context;
try {
// get your user
if (user.authenticated === undefined) {
return {
redirect: {
permanent: false,
destination: `/`,
},
};
}
return {
props: {
// any static props you want to deliver to the component
},
};
} catch (e) {
console.error("uh oh");
return;
}
}
Good luck!

Uncaught TypeError: Cannot read properties of null (reading 'uid') : Get this error but application work fine?

I get this error upon logging out: (Uncaught TypeError: Cannot read properties of null (reading 'uid')), but my application works and does what I want it to do.
Within my logout function I am basically deleting an API access token from the user doc in the database when user logouts, because the token does not expire so it is added on login and removed on logout as part of authentication process. This all works fine.
so how do I get rid of this error, is there another way I can structure my code to get rid of this error?
It makes sense that this error appears once user has logged out because the uid is longer available to access due to user not being active (current user).
Many thanks, code below.
import { signOut } from "#firebase/auth";
import { useNavigate } from "react-router";
import { auth, db } from "../../firebase";
import { doc, updateDoc, deleteField } from "firebase/firestore";
export const Logout = () => {
const user = auth.currentUser;
const uid = user.uid;
console.log(uid);
const userRef = doc(db, 'users', uid);
const navigate = useNavigate();
const logoutUser = async () => {
//Deleting mavenlink access token
await updateDoc(userRef, {
accessToken: deleteField()
});
signOut(auth).then(() => {
navigate("/")
})
}
return {logoutUser}
};
For context below is code where authenticated routes are handled in app.js
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [state, setState] = useState({});
const [user, loading, error] = useAuthState(auth);
//Hook to handle authentication
useEffect(() => {
if (loading) {
// maybe trigger a loading screen
return;
}
if (user && !isAuthenticated){
setIsAuthenticated(true);
console.log("logged in");
} else if (!user && isAuthenticated) {
setIsAuthenticated(false);
console.log("logged out");
}
}, [user, loading]);
const unauthenticatedRoutes = useRoutes([
{ path: "/", element: <LoginForm/> },
{ path: "/reset", element: <ResetForm/> },
{ path: "/register", element: <RegisterForm/> },
]);
return (
<AppContext.Provider value={{
isAuthenticated, setIsAuthenticated,
}}>
<div className="App">
{!isAuthenticated ? <>{unauthenticatedRoutes}</> : <Sidebar/>}
</div>
</AppContext.Provider>
);
}
export default App;
Note: the authenticated routes are defined in the sidebar component and rendered there.
You may add privateRoutes to resolve this issue.
What I mean by this is create a file where you specify the condition whether your component should be rendered, or it should redirect to signin page.
And you can convert your normal route to private route. wrap your element in private route in your normal route. When you make your signout function a private component it will redirect the user to signin page as now user is deleted and currentUser no longer exist.
import React from 'react';
import { Navigate} from 'react-router-dom';
//import user from firebase
export default function PrivateRoute({ children }) {
const { currentUser } = // user imported
return currentUser ? children : <Navigate to="/signin" />;
}

Need react context to update before redirect

I have a UserContext that is set when App renders. App retrieves the current user from a server, and then sets the user context in a provider.
So whenever I navigating to a link, App renders, get's the current user from the server, and sets it. This allows all the children have access to the user.
Problem with <Redirect>
But I'm running into a problem when I use <Redirect>.
If the user updates on the server, then App needs to re-render in order to get the updated user object.
But on a redirect App doesn't re-render which leads to an outdated user context until the user refreshes the page in order to re-render App.
Example: Login to see your profile
In my code below I have a login button. When the user logs in the page redirects to their profile.
But even though the user is successfully logged in on the server, the user context hasn't updated. This is because redirect doesn't re-render App.
Is there a way to get redirect to re-render app or some other solution?
Code
The relevant code is below.
The full code is available on the repo here. Download, run npm i, npm start, and then either select and play Compounded Server/React in the debugger or run node currentUserServer/server.js to start the server without the debugger tools.
Frontend
App.js
import React, { useEffect, useContext, useState } from "react";
import { UserContext } from "./contexts/UserContext";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Login from "./Login";
import Profile from "./Profile";
const currentUser = async () => {
const user = await fetch("/users/current", {}).then(async (res) => {
const userJson = await res.json();
return userJson;
});
return user;
};
export default function App() {
const [user, setUser] = useState(null);
useEffect(() => {
currentUser().then((user) => {
setUser(user);
});
}, []);
return (
<Router>
<div className="App">
<UserContext.Provider value={user}>
<Switch>
<Route path="/profile">
<Profile />
</Route>
<Route path="/">
<Login />
</Route>
</Switch>
</UserContext.Provider>
</div>
</Router>
);
}
Login.js
import React, { useContext, useState } from "react";
import { Redirect } from "react-router-dom";
import { UserContext } from "./contexts/UserContext";
export default function Login() {
const [next, setNext] = useState(false);
const currentUser = useContext(UserContext);
return (
<div>
Logged In:{" "}
{!currentUser || currentUser.message === "not logged in"
? "No One"
: currentUser.username}{" "}
<br></br>
<button
onClick={() => {
fetch("/login", { method: "POST" }).then((res) => {
if (res.status === 201) setNext(true);
});
}}
>
Login
</button>
<button
onClick={() => {
fetch("/logout", { method: "DELETE" });
}}
>
Logout
</button>
{next && <Redirect to="/profile" />}
</div>
);
}
Profile.js
import React, { useContext } from "react";
import { UserContext } from "./contexts/UserContext";
export default function Profile() {
const currentUser = useContext(UserContext);
return (
<div>
{currentUser && !currentUser.message
? "You're logged in and can edit your profile."
: "You're not logged in."}
</div>
);
}
I found two solutions solution.
#1 Quick fix but not as good: setRefresh
This works but seems to nullify the whole point of using context because we have to pass the state down.
I created a refresh state on App.js that triggers useEffect.
useEffect(() => {
if (refresh) {
currentUser().then((user) => {
setUser(user);
setRefresh(false);
});
}
}, [refresh]);
Now I can pass setRefresh as a prop to Login in order trigger useEffect to run again.
Full code here
#2 Better! setUser and getUser in context
This is sort of like the solution above but makes it so you're still taking advantage of context. You put setUser and getUser method on the context- inspired by this answer.
In app.js we create a user state. We set the value of UserContext to an object. The object contains user and setUser. And we also pass in getUser, a function that request the current user from the server.
export default function App() {
const [user, setUser] = useState(null);
const getUser = async () => {
const user = await fetch("/users/current", {}).then(async (res) => {
const userJson = await res.json();
return userJson;
});
return user;
};
const value = { user, setUser, getUser };
Here the object is passed to the provider.
<UserContext.Provider value={value}>
Now we have access to user, setUser, getUser anywhere there is a UserContext consumer.
const { user, setUser, getUser } = useContext(UserContext);
We can use getUser to ping the server for the current user. Then set that result as the user.
setUser(await getUser());
export default function Login(props) {
const [next, setNext] = useState(false);
const { user, setUser, getUser } = useContext(UserContext);
return (
<div>
Logged In:{" "}
{!user || user.message === "not logged in" ? "No One" : user.username}{" "}
<br></br>
<button
onClick={() => {
fetch("/login", { method: "POST" }).then(async (res) => {
if (res.status === 201) {
setUser(await getUser());
setNext(true);
}
});
}}
>
Login
</button>
Full Code here

Unnecessary rerenders using Apollo + useContext for Authentication (SSR)

I'm experiencing rerenders seemingly as a result of useQuery being called when I don't want it to be.
First load, isAuthenticated's state is instantiated via a function that makes a query to the server to get the userId based on the attached header.
If a valid header is present, isAuthenticated is set to true and therefore useMemo is called (as isAuthenticated is a dependency). useMemo calls setUser, which puts the userId and role into the context, to be passed down through the AuthProvider (in _app, wrapping everything but the ApolloProvider).
I use the userId in the Navbar (via useContext) to get the avatar (and other things in the rest of the app). What I've noticed is that any time I click on something (for example, a separate query filtering products), the CURRENT_USER query in _app is called. And this manifests as the Navbar losing the useId data passed to it for a second, and then reverts back. So, everytime I click on things, my Navbar is rerendering. I don't understand why the CURRENT_USER query keeps being called...
For context, I use the same setup in another app that does not have SSR without issue.
auth.context.js
import React, { useState, useEffect, useMemo} from "react";
import { CURRENT_USER } from "graphql/query/customer.query";
import { gql, useQuery } from "#apollo/client";
import { isBrowser } from "components/helpers/isBrowser";
export const AuthContext = React.createContext({});
export const AuthProvider = ({ children }) => {
const { data, error, loading } = useQuery(CURRENT_USER, {
onCompleted: () => {
console.log("called query", data);
},
ssr: true,
});
const isValidToken = () => {
if (isBrowser) {
const token = localStorage.getItem("token");
if (error) {
return false;
}
if (!loading && !data) {
return false;
}
if (token && data?.currentUser) {
console.log(data.currentUser);
return true;
}
if (!token) {
return false;
}
}
};
const [isAuthenticated, makeAuthenticated] = useState(isValidToken());
const [user, setUser] = useState({});
function authenticate() {
console.log(isAuthenticated);
makeAuthenticated(isValidToken());
}
useEffect(() => {
console.log("attempted to run");
if (data && isAuthenticated) {
setUser(data.currentUser);
console.log("user", user);
}
}, [isAuthenticated]);
function signout() {
makeAuthenticated(false);
localStorage.removeItem("token");
}
return (
<AuthContext.Provider
value={{
isAuthenticated,
authenticate,
signout,
user,
}}
>
{children}
</AuthContext.Provider>
);
};
"#apollo/client": "^3.0.0-beta.44",
"next": "^9.3.6",
"react": "^16.13.1",

React context api update state instantly

I'm using the context api in a Gatsby setup to keep track of a state called userIsLoggedIn. I'm using Firebase for authentication.
This is my context file:
import { createContext } from "react"
export const AppContext = createContext(null)
This is my AppWrapper component:
import React, { useState, useEffect } from "react"
import firebase from "../../config/firebase"
import { AppContext } from "../../context/AppContext"
const AppWrapper = ({ children }: any) => {
const [userIsLoggedIn, setUserIsLoggedIn] = useState(false)
const authListener = () => {
firebase.auth().onAuthStateChanged(user => {
if (user && user.emailVerified) {
setUserIsLoggedIn(true)
} else {
setUserIsLoggedIn(false)
}
})
}
useEffect(() => {
authListener()
}, [])
return (
<>
<AppContext.Provider
value={{
userIsLoggedIn,
}}
>
<main>{children}</main>
</AppContext.Provider>
</>
)
}
export default AppWrapper
This is my index page where I want to keep track if the user is logged in so I can show/hide certain content:
import React, { useContext } from "react"
import { AppContext } from "../context/AppContext"
const IndexPage = () => {
const app = useContext(AppContext)
console.log("app", app)
return (
<>
{app && app.userIsLoggedIn && (
<>
<h1>Hello dearest user</h1>
<p>Welcome to your page.</p>
</>
)}
</>
)
}
export default IndexPage
The outcome of my console.log inside the my IndexPage component is the following when I first load the page or whenever the page is reloaded:
app {userIsLoggedIn: false}
app {userIsLoggedIn: true}
This means my page is re-rendering and my content is flickering between content which is hidden/shown when a user is logged in. Is there a way to avoid this and make the state more instant? I'm open for any suggestions :)
Okay so I found out what helps my specific case. Instead of using the context api to keep an app state (which will be reset to it's default value when reloaded, hence the flickering between states) I use localStorage to save if a user is logged in in combination with my authListener function.
This is the auth util I added:
// Firebase
import firebase from "../config/firebase"
export const isBrowser = () => typeof window !== "undefined"
export const getUser = () => {
if (isBrowser()) {
const user = window.localStorage.getItem("user")
if (user !== null) {
return JSON.parse(user)
} else {
return ""
}
}
}
export const setUser = (email: string | null) =>
isBrowser() && window.localStorage.setItem("user", JSON.stringify(email))
export const isLoggedIn = () => {
const user = getUser()
return !!user
}
export const logout = () => {
return new Promise(resolve => {
firebase
.auth()
.signOut()
.then(() => {
setUser("")
resolve()
})
})
}
and inside my AppWrapper my authListener function now looks like this:
import { setUser, logout } from "../../utils/auth"
const authListener = () => {
firebase.auth().onAuthStateChanged(user => {
if (user && user.emailVerified) {
setUser(user.email)
} else {
logout()
}
})
}
useEffect(() => {
authListener()
})
Anywhere in my app I can use the isLoggedIn() util to check if the user is actually logged in without having the flickering content.
If I manually delete or alter the localStorage user this will instantly be refreshed by the authListener function when anything changes in the app.

Categories