I am trying to learn how to effectively use React Hooks but am having an issue. I would like to reflect whether or not a user is "logged in" to the site using a JWT in local storage. When I first visit the page, the hook works as I intend, retrieving the user data. But if I click the "Log Out" button in the example below, the component does not update to reflect this, although it will if I refresh the page. How might I properly implement this hook to get it to update when logging in/out?
Custom hooks:
export const useUser = () => {
const [token] = useToken();
const getPayloadFromToken = token => {
const encodedPayload = token.split('.')[1];
return JSON.parse(atob(encodedPayload))
}
const [user,setUser] = useState(() => {
if(!token) return null;
return getPayloadFromToken(token);
})
useEffect(() => {
if(!token) {
setUser(null);
} else {
setUser(getPayloadFromToken(token));
}
}, [token]);
return user;
}
export const useToken = () => {
const [token, setTokenInternal] = useState(() => {
return localStorage.getItem('token');
});
const setToken = newToken => {
localStorage.setItem('token',newToken);
setTokenInternal(newToken);
}
return [token, setToken];
}
Navigation Bar Component:
const NavigationBar = () => {
const user = useUser();
const logOut = () => {
localStorage.removeItem('token');
};
return(
<>
<div>{user ? 'logged in' : 'logged out'}</div>
<button onClick={logout}>Log Out</button>
</>
);
}
I think at a minimum you could expose out the setToken function directly via the useUser hook and when you call logout call setToken(null) (or similar) and this would be sufficient enough to trigger a render. Ideally though you'd have all this authentication "state" centrally located in a React context so the hooks all reference the same single state.
I suggest actually encapsulating the logout function within the useUser hook and exposing that out instead of directly exposing the setToken function. You want the hooks to maintain control over the state invariant and not rely on consumers to pass/set the correct state values.
Example:
export const useUser = () => {
const [token, setToken] = useToken();
const getPayloadFromToken = token => {
const encodedPayload = token.split('.')[1];
return JSON.parse(atob(encodedPayload));
}
const [user, setUser] = useState(() => {
if (!token) return null;
return getPayloadFromToken(token);
});
useEffect(() => {
if (!token) {
setUser(null);
} else {
setUser(getPayloadFromToken(token));
}
}, [token]);
const logout = () => {
setUser(null);
setToken(null);
};
return { user, logout };
}
...
export const useToken = () => {
const [token, setTokenInternal] = useState(() => {
// Initialize from localStorage
return JSON.parse(localStorage.getItem('token'));
});
useEffect(() => {
// Persist updated state to localStorage
localStorage.setItem('token', JSON.stringify(newToken));
}, [token]);
const setToken = newToken => {
setTokenInternal(newToken);
}
return [token, setToken];
};
...
const NavigationBar = () => {
const { logout, user } = useUser();
return(
<>
<div>{user ? 'logged in' : 'logged out'}</div>
<button onClick={logout}>Log Out</button>
</>
);
}
I encountered something similar. A solution I found that worked for me was to save whether or not the user is logged in using Context. Effectively this would involve creating a wrapper around components in your app which need access to whether or not a user is logged in as an alternative to using local storage to save this sort of stuff.
https://reactjs.org/docs/context.html
Related
Hi I am trying to create an authentication app. To do that I create login page and DashBoardPage.
Here is my login code. In My login page code, handleSubmit and submit works correctly, and setAuthenticated, localStorage.setItem("authenticated", true) set true correctly.
function Login() {
useEffect(() => {
localStorage.setItem("authenticated", false)
setAuthenticated(false)
}, []);
//const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("")
const [authenticated, setAuthenticated] = useState(localStorage.getItem("authenticated") || false)
const handleSubmit = async () =>{
try{
let res = await api.post("/login", {"email": email, "password":password} )
return res
}catch(e){
console.log("Something Went Wrong")
}
}
const submit = (e) =>{
if(!email.match(validEmailRegex)){
console.log("Not Valid Mail Address")
}
handleSubmit()
.then(res => {
console.log(res.data.token)
console.log(res.data.message)
setAuthenticated(true) // It shows that it is authenticated
localStorage.setItem("authenticated", true)
})
.catch(error => console.log(error))
}
After I entered true values in login page, I go to "/DashBoardPage" via using inspector panel.
Here is my DashBoard code
import { useEffect, useState } from "react";
import { Redirect } from "react-router-dom";
const Dashboard = () => {
const [authenticated, setAuthenticated] = useState(null);
useEffect(() => {
console.log("Use Effect First Entered",localStorage.getItem("authenticated"))
const loggedInUser = localStorage.getItem("authenticated");
console.log("Logged in user ", loggedInUser)
if (loggedInUser) {
setAuthenticated(true);
}
else {
setAuthenticated(false)
}
}, []);
console.log("const Dashboard Entered",localStorage.getItem("authenticated"))
console.log("EXIT")
console.log("One Before Return", authenticated)
if (!authenticated) {
return <Redirect replace to="/TestPage" />;
}
if(authenticated) {
return (
<Redirect replace to="/MyProfilePage" />
);
}
};
export default Dashboard;
My problem here is authenticated blocks always remain null, even though I am trying to change their value in useEffect before rendering. As a reason for that, I cannot goto MyProfile page and always go back to TestPage. Can someone explain why it is happening?
Function in the useEffect is executed after render phase of the component. So redirect is happening before useEffect.
Add if (authenticated === null) return null; just before the rest of your ifs
Additional note: setXXX functions from useState hook are not updating the XXX vairable immediately, in-place. The variable will be updated on next render only.
it is my first time using redux, I have been trying to activate an account through mail with redux, but I didn't know how to consume the method
here is my redux activate method
export const activateAccount= createAsyncThunk('user/activateUser', async (data, { rejectWithValue }) => {
try
{
const token = window.location.href.slice(window.location.href.indexOf('?') + 1);
const config = {
method: 'put',
url: `${API_ENDPOINT}/v1/api/account/${token}/enable`,
data,
};
const payload = await axios(config);
return payload.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
});
here how I try to use it
function EnableAccount()
{
const dispatch = useDispatch();
useEffect(() => {
const Activate = (entry) => {
dispatch(
activateAccount({
...entry,
}),
).then(unwrapResult);
};
Activate();
}, []);
return <Result status="success" title="Successfully Activated Account " />;}
and here im calling it in the route
<Route exact path="/ActivateAccount/:token">
<EnableAccount />
</Route>
and this one is from my html template
<a href="${API_ENDPOINT}/ActivateAccount/token=?${confirmationCode}/enable"
>I want to activate my account
</a>
and thank you.
I am trying to show information of a state into a component. I am using the context to load information from different origin.
const router = useRouter()
const [sidebarOpen, setSidebarOpen] = useState(false)
const [loadUser, setLoadUser] = useState({})
const { state, dispatch } = useContext(Context)
const { user } = state
useEffect(() => {
if (user == null) {
router.push("/auth/login")
}
setLoadUser(user)
}, [user])
That code is inside a dashboard component. The idea is to get the user information into the state to show it on the dashboard. The problem is that the useEffect is executed at the same time that the content is rendered, therefore it does not have time to load the information, and the variables are null for me.
Here is an image of how the loadState variable behaves once inside the render.
I am using nextjs by the way.
I am passing the context with a provider to the App. And wrapped it.
// initial state
const initialState = {
user: null
};
// Create context
const Context = createContext()
// root reducer
const reducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case "LOGIN":
return { ...state, user: payload};
case "LOGOUT":
return { ...state, user: null };
default:
return state;
}
}
// context provider
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const router = useRouter();
useEffect(() => {
dispatch({
type: "LOGIN",
payload: JSON.parse(window.localStorage.getItem("user"))
})
}, [])
axios.interceptors.response.use(
function (response) {
return response
},
function (error) {
let res = error.response;
console.log(error)
if (res.data.status === 401 && res.config && !res.config.__isRetryRequest) {
return new Promise((resolve, reject) => {
axios.get(`${process.env.NEXT_PUBLIC_API}/auth/logout`)
.then(res => {
dispatch({ type: "LOGOUT" })
window.localStorage.removeItem("user")
router.push('/auth/login')
})
.catch(error => {
reject(error)
})
})
}
return Promise.reject(error);
}
)
useEffect(() => {
const getCsrfToken = async () => {
const { data } = await axios.get(`${process.env.NEXT_PUBLIC_API}/csrf-token`)
axios.defaults.headers["X-CSRF-Token"] = data.csrfToken
}
getCsrfToken()
}, [])
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
)
}
export { Context, Provider }
Add dependecy array. That shoud be quick fix.
useEffect(() => {
if (user == null) {
router.push("/auth/login")
}
setLoadUser(user)
}, [state]) // dependecy array looks at state changes
You can read more about dependecy array on the official react documentation: https://reactjs.org/docs/hooks-effect.html
proper fix:
whenever you are using api calls in your app, those are asyncrhonus. You should use some loading state.
for example
const Context = () => {
const [loading, setLoading] = useState(true)
const [user, setUser] = useState(true)
fetch('api/user').than(r=> setLoading(false);setUser(r.user))
// rest of the context code
}
and in component itself:
const state = useContext(Context)
useEffect(() => {
state.loading ? null : setLoadUser(user)
}, [state]) //
So I'm creating authentication logic in my Next.js app. I created /api/auth/login page where I handle request and if user's data is good, I'm creating a httpOnly cookie with JWT token and returning some data to frontend. That part works fine but I need some way to protect some pages so only the logged users can access them and I have problem with creating a HOC for that.
The best way I saw is to use getInitialProps but on Next.js site it says that I shouldn't use it anymore, so I thought about using getServerSideProps but that doesn't work either or I'm probably doing something wrong.
This is my HOC code:
(cookie are stored under userToken name)
import React from 'react';
const jwt = require('jsonwebtoken');
const RequireAuthentication = (WrappedComponent) => {
return WrappedComponent;
};
export async function getServerSideProps({req,res}) {
const token = req.cookies.userToken || null;
// no token so i take user to login page
if (!token) {
res.statusCode = 302;
res.setHeader('Location', '/admin/login')
return {props: {}}
} else {
// we have token so i return nothing without changing location
return;
}
}
export default RequireAuthentication;
If you have any other ideas how to handle auth in Next.js with cookies I would be grateful for help because I'm new to the server side rendering react/auth.
You should separate and extract your authentication logic from getServerSideProps into a re-usable higher-order function.
For instance, you could have the following function that would accept another function (your getServerSideProps), and would redirect to your login page if the userToken isn't set.
export function requireAuthentication(gssp) {
return async (context) => {
const { req, res } = context;
const token = req.cookies.userToken;
if (!token) {
// Redirect to login page
return {
redirect: {
destination: '/admin/login',
statusCode: 302
}
};
}
return await gssp(context); // Continue on to call `getServerSideProps` logic
}
}
You would then use it in your page by wrapping the getServerSideProps function.
// pages/index.js (or some other page)
export const getServerSideProps = requireAuthentication(context => {
// Your normal `getServerSideProps` code here
})
Based on Julio's answer, I made it work for iron-session:
import { GetServerSidePropsContext } from 'next'
import { withSessionSsr } from '#/utils/index'
export const withAuth = (gssp: any) => {
return async (context: GetServerSidePropsContext) => {
const { req } = context
const user = req.session.user
if (!user) {
return {
redirect: {
destination: '/',
statusCode: 302,
},
}
}
return await gssp(context)
}
}
export const withAuthSsr = (handler: any) => withSessionSsr(withAuth(handler))
And then I use it like:
export const getServerSideProps = withAuthSsr((context: GetServerSidePropsContext) => {
return {
props: {},
}
})
My withSessionSsr function looks like:
import { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next'
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'
import { IronSessionOptions } from 'iron-session'
const IRON_OPTIONS: IronSessionOptions = {
cookieName: process.env.IRON_COOKIE_NAME,
password: process.env.IRON_PASSWORD,
ttl: 60 * 2,
}
function withSessionRoute(handler: NextApiHandler) {
return withIronSessionApiRoute(handler, IRON_OPTIONS)
}
// Theses types are compatible with InferGetStaticPropsType https://nextjs.org/docs/basic-features/data-fetching#typescript-use-getstaticprops
function withSessionSsr<P extends { [key: string]: unknown } = { [key: string]: unknown }>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) {
return withIronSessionSsr(handler, IRON_OPTIONS)
}
export { withSessionRoute, withSessionSsr }
i'm using Passport-local for authentication and Gatsby on front end
Generally, the code works fine. When I click on signout, the server returns a 200 call and I get a response "User sign out successfully". I'm then navigated to the signin page. From there, I am unable to access my Post page which is private route. My signin and post page are client side routes
The issue comes when I click on the home page (which is a static page). From there, when I click on the post link, I'm navigated to the post page which supposedly is inaccessible now that I have signed out. My fetchuser action creator runs and is able to fetch the user detail even though I have already signed out from my app
Anyone knows how to resolve this issue? Thanks in advance
SERVER
signout api
router.get("/signout", (req, res) => {
req.logout();
res.send("Sign Out Successfully");
});
me api
router.get("/me", (req, res) => {
res.send(req.user);
});
CLIENT
app
const App = () => {
useEffect(() => {
store.dispatch(fetchUser())
}, [])
return (
<Layout>
<Alert />
<Router basepath="/app">
<Signin path="/signin" />
<Signup path="/signup" />
<PrivateRoute path="/post" component={Post} />
{/* <Default path="/" /> */}
</Router>
</Layout>
)
}
export default App
fetchUser action creator
export const fetchUser = () => async dispatch => {
try {
const res = await axios.get("http://localhost:5000/api/users/me", {
withCredentials: true,
})
dispatch({
type: FETCH_USER,
payload: res.data,
})
} catch (err) {
console.log(err)
dispatch({
type: AUTH_ERROR,
})
}
}
signout action creator
export const signOut = () => async dispatch => {
const res = await axios.get("http://localhost:5000/api/users/signout")
console.log(res)
dispatch({
type: SIGNOUT,
})
navigate("/app/signin")
}
I think your approach is correct and valid, despite personally thinking that handling it with cookies or localStorage could be easily maintained.
Your <PrivateRoute> component should handle your logic and perform some actions depending on the user state (logged or not), something like:
import React from "react"
import { navigate } from "gatsby"
import { isLoggedIn } from "../services/auth"
const PrivateRoute = ({ component: Component, location, ...rest }) => {
if (!isLoggedIn() && location.pathname !== `/app/login`) {
navigate("/app/login") // or your desireed page
return null
}
return <Component {...rest} />
}
export default PrivateRoute
Your auth service, should handle your requests, in this case using localStorage but it can be replaced for your API requests:
export const isBrowser = () => typeof window !== "undefined"
export const getUser = () =>
isBrowser() && window.localStorage.getItem("gatsbyUser")
? JSON.parse(window.localStorage.getItem("gatsbyUser"))
: {}
const setUser = user =>
window.localStorage.setItem("gatsbyUser", JSON.stringify(user))
export const handleLogin = ({ username, password }) => {
if (username === `john` && password === `pass`) {
return setUser({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
})
}
return false
}
export const isLoggedIn = () => {
const user = getUser()
return !!user.username
}
export const logout = callback => {
setUser({})
callback()
}