I'm trying to produce a minimal example of routing to login if no session is found. Here is my code from _app.js inside pages folder :
function MyApp({ Component, pageProps }) {
const [user, setUser] = useState(null)
const router = useRouter()
useEffect(() => {
const session = document.cookie.includes("session_active=true")
if (session) {
fetch("/api/user")
.then(u => u.json().then(setUser))
} else {
const redirectURI = router.pathname
const url = {pathname: "/login", query: {"redirect_uri": redirectURI}}
router.push(url)
}
}, [])
if (!user) return Loading()
return (<div>User {user.name} {user.surname}</div>)
}
My login is inside pages/login.js with this content :
const Login = () => (<div>Login page</div>)
export default Login
However it's stuck on the loading page even though I don't have the session. Am I misusing the router ?
The URL is changed properly to /login?redirect_uri=%2Ffoo but the content is not the one from my Login
Below is a stackblitz reproduction: https://stackblitz.com/edit/github-supacx-rpl5rm
I see the problem, You are preventing the app to load.
You are not changing user's state in case there is no session_active cookie.
You are trying to render the only loading component instead of the next App.
if (!user) return Loading()
Solution:
Let the app render
render the loading component inside the return statement of the app component
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/router'
export default function App({ Component, pageProps }) {
const [user, setUser] = useState(null)
const router = useRouter()
useEffect(() => {
const session = document.cookie.includes('session_active=true')
if (session) {
fetch('/api/user').then((u) => u.json().then(setUser))
} else {
setUser(true) // set to true.
const redirectURI = router.pathname
const url = { pathname: '/login', query: { redirect_uri: redirectURI } }
router.push(url)
}
}, [])
return (
<>
{!user && <div>loading</div>}
<Component {...pageProps} />
</>
)
}
I am not sure which approach you will use to pass user info to all components. My suggestion would be to create a context for authentication and wrap the app with it. Then handle the user session and redirection in the context.
Related
I'm working on a web application with react and Next.js and I also have a Node.js API separated as a back-end.
I have a login form where I send the data to the API to recover JWT, when I do that, everything works fine, but after redirecting the user to the protected route "dashboard", or after a refresh the user context gets lost.
Here is the protected route :
import React, {useContext, useEffect} from 'react'
import { useRouter } from "next/router";
import { Context } from '../../context/context';
export default function DashboardIndexView() {
const router = useRouter();
const {isUserAuthenticated, setUserToken, userToken} = useContext(Context);
useEffect(() => {
isUserAuthenticated()
? router.push("/dashboard")
: router.push("/authentication/admin");
}, []);
return (
<>
<h1>Dashboard index view</h1>
</>
)
}
and here is the context file :
import React, {createContext, useEffect, useState} from 'react'
import { useRouter } from "next/router";
import axios from 'axios'
export const Context = createContext(null)
const devURL = "http://localhost:4444/api/v1/"
export const ContextProvider = ({children}) => {
const router = useRouter()
const [user, setUser] = useState()
const [userToken, setUserToken] = useState()
const [loading, setLoading] = useState(false)
const [successMessage, setSuccessMessage] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const Login = (em,pass) => {
setLoading(true)
axios.post(devURL+"authentication/login", {
email : em,
password : pass
})
.then((res)=>{
setSuccessMessage(res.data.message)
setErrorMessage(null)
setUser(res.data.user)
setUserToken(res.data.token)
localStorage.setItem('userToken', res.data.token)
localStorage.setItem('user', res.data.user)
setLoading(false)
})
.catch((err)=>{
setErrorMessage(err.response.data.message)
setSuccessMessage(null)
setLoading(false)
})
}
const Logout = () => {
setUserToken()
setUser()
localStorage.setItem('userToken', null)
localStorage.setItem('user', null)
router.push('/authentication/admin')
}
const isUserAuthenticated = () => !!userToken
return (
<Context.Provider value={{
Login,
user,
loading,
userToken,
setUserToken,
Logout,
successMessage,
setSuccessMessage,
setErrorMessage,
isUserAuthenticated,
errorMessage}}>
{children}
</Context.Provider>
)
}
How can I keep the user on the dashboard page even when a refresh happens ?
It's normal for useContext() to lose its value on a page refresh. Contexts don't persist any data, they simply share the data between components. In, Next.js, it can work between pages because Next.js handles navigation on the client side. But as you've noticed, as soon as you refresh, the app is mounted from scratch and this time the context never gets the value of the JWT because the JWT was never sent on this new instance of your app.
The solution, at a high-level, is to store the JWT somewhere (localStorage or cookie) and inject the value in your Context.Provider. You're already setting the values in localStorage now you just need a useEffect that will read them and add them to the context:
useEffect(() => {
setUser(localStorage.get('user'));
setUserToken(localStorage.get('userToken'));
}, []);
But the real solution, in my opinion, is to use https://next-auth.js.org/ instead. It handles security concerns and is a well-known library for Next.js
I am practicing AWS' Cognito. For front-end I am using React and for routing I am using React-router-dom. For Cognito validation I am using amazon-cognito-identity-js package. My Congito signin, signup and confirmation logic works fine. I made one helper function where I validate the Congnito. and reuse it in different component. I split my Nav bar into two components. From Congnito current user I made one callback function and use it in useEffect, and dependencies put the callback function, by default getAuthenticatedUser is null. I add condition where it fetch the data, if getAuthenticatedUser then redirect to signin and signup page. Because of this condition I am getting the error: Can't perform a React state update on an unmounted component...... Also when I signed in it does not change the nav bar name, I have to refresh the browser then I can see the change. I share my code in codesandbox.
This is my helper function
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { CognitoUserPool } from 'amazon-cognito-identity-js';
const Pool_Data = {
UserPoolId: 'us-east-1_IEyFfUupx',
ClientId: '63fc9g5c3g9vhqdalrv9eqhoa2',
};
export default function useHandler() {
const [state, setstate] = useState({
loading: false,
isAuthenticated: false
})
const { loading, isAuthenticated } = state;
const userPool = new CognitoUserPool(Pool_Data)
const getAuthenticatedUser = useCallback(() => {
return userPool.getCurrentUser();
},
[],
);
console.log(getAuthenticatedUser());
useEffect(() => {
getAuthenticatedUser()
}, [getAuthenticatedUser])
const signOut = () => {
return userPool.getCurrentUser()?.signOut()
}
console.log(getAuthenticatedUser());
return {
loading,
isAuthenticated,
userPool,
getAuthenticatedUser,
signOut
}
};
This is my navigation
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import SigninLinks from './SigninLinks';
import SignoutLinks from './SignoutLinks';
import useHandlder from '../configHandler/useHandler';
const Nav = () => {
const { getAuthenticatedUser } = useHandlder();
const Links = getAuthenticatedUser() ? <SigninLinks /> : <SignoutLinks />
return (
<nav className="nav-wrapper grey darken-3">
<div className="container">
<h2 className="brand-logo">Logo</h2>
{
Links
}
</div>
</nav>
);
};
export default Nav;
This is Home screen where it display the data and getting error
import React, { useState, useEffect } from "react";
import { api } from './api';
import useHandlder from './configHandler/useHandler'
import { Redirect } from 'react-router-dom';
const Home = () => {
const [state, setstate] = useState([]);
const { getAuthenticatedUser } = useHandlder();
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const data = await response.json();
setstate(data)
}
return getAuthenticatedUser() === null ? <Redirect to="/signin" /> : //In here is the //error happening.
<div className="row">
<h1>hello welcome to home</h1>
{
state?.map((i: string, id: number) => <h1 key={id}>{i.title}</h1>)
}
</div>
};
export default Home;
Issue
The issue is your app starts on the home ("/") path and renders the Home component. Home initiates a GET request upon mounting and checks for an authenticated user, and if there is none, renders a redirect to your "/signin" route.
The fetch is asynchronous so when the redirect occurs the GET request is resolving after Home has been unmounted and it tries to update the local state with the response data, but can't.
Solution
You need to use an Abort Controller to cancel in-flight requests. If the component unmounts, an effect cleanup function cancels the fetch request. In Home update the useEffect hook to create an AbortController and signal to be used in a cleanup function.
useEffect(() => {
const controller = new AbortController(); // <-- create controller
const { signal } = controller; // <-- get signal for request
const fetchData = async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts`,
{ signal } // <-- pass signal with options
);
const data = await response.json();
setstate(data);
};
fetchData();
return () => controller.abort(); // <-- return cleanup function to abort
}, []);
Demo
In my react application, I am trying to implement Public and Private routes with react-router-dom.
I am currently getting the authentication state from firebase.auth().onAuthStateChanged() function.
The problem is that since the firebase.auth().onAuthStateChanged() function is asynchronous, JSX block is rendered with authState of false first, and then firebase.auth().onAuthStateChanged() sets authState to true after the JSX is returned.
So, the authentication is true, but my react app stays at the sign-in page.
PublicRoute.js
const PublicRoute = ({ component: Component, restricted, ...rest }) => {
let authState = false;
getAuthState()
.then(user => {
if (user.uid) {
authState = true;
} else {
authState = false;
}
})
return (
<Route { ...rest } render={ props => (
authState && restricted
? <Redirect to="/" />
: <Component { ...props } />
) } />
)
};
export default PublicRoute;
getAuthState.js
export const getAuthState = () => {
return new Promise((resolve, reject) => {
const waitAuthStateChange = () => {
let currentUser = firebase.auth().currentUser;
if (currentUser === null) {
firebase.auth().onAuthStateChanged(user => currentUser = user);
setTimeout(waitAuthStateChange, 100);
} else {
resolve(currentUser);
}
}
waitAuthStateChange();
})
};
I don't know how to make re-render or re-return the JSX after the authentication is fetched by firebase.auth().onAuthStateChanged() listener.
Thank you in advance!!
You will need a React state.
To use it, you can use it with a hook called useState();
Example,
const [authState,setAuthState] = useState(false);
getAuthState()
.then(user => {
if (user.uid) {
setAuthState(true);
}
})
Your current authState is a normal javascript object, so React doesn't regard it as a change and therefore, won't re-render.
Change it to React state and then when you do a setAuthState() , React use it as a pivot for change and will render again.
Side Note: Calling getAuthState() by itself is not a good practice in React.
In React way, it should be put inside the useEffect() hook.
Reference for useState: https://reactjs.org/docs/hooks-state.html
Reference for useEffect: https://reactjs.org/docs/hooks-effect.html
Iam trying to get the string from localstorage and check that with some condition and redirect the page based on the value from localstorage
but the problem is, i could see the page for couple of seconds even before it redirects to origin page
basically its authentication kind of thing
here is the code
import React from "react";
import AdminDashboard from "../components/AdminDashboard";
import Router from "next/router";
import SignIn from "../pages/index";
import useRouter from "next/router";
import fetch from "isomorphic-unfetch";
var userType;
export default function Test({ token }) {
const [userType, setuserType] = React.useState(false);
React.useLayoutEffect(() => {
if (userType !== true) {
const lt = localStorage.getItem("userType");
if (lt !== true) Router.push("/");
}
return () => {};
}, []);
{
console.log(token);
}
return (
<div>
<AdminDashboard>Admin Page </AdminDashboard>
</div>
);
}
You can render the page in the client only to avoid page flashing.
On the other hands, You can just using useEffect.
const Page = null;
React.useEffect(() => {
if (userType !== true) {
const lt = localStorage.getItem("userType");
if (lt !== true) Router.push("/");
} else {
Page = (
<div>
<AdminDashboard>Admin Page </AdminDashboard>
</div>
)
}
}, [])
return Page
I have the following (redux) state:
{
authentication: user
}
When logged out, user is set to null.
I have the following components:
const Dashboard = ({ authentication }) => {
if (!authentication.user) {
return <Redirect to={"/login"} />
}
return (
<SomeInnerComponent />
);
}
const SomeInnerComponent = ({ authentication }) => {
const name = authentication.user.name;
return (
<h1>Hello, {name}</h1>
)
}
authentication is mapped using connect and mapStateToProps. I would think that when I am logged out that I would be redirected, but I get an error instead: authentication.user is null.
Why does the if-statement in Dashboard not redirect me? I also tried wrapping it in a useEffect with authentication as a dependency.
In our app, we redirect unauthenticated users by history.replace history docs
or you read docs again, maybe you can find mistake in your code reacttraining
I fixed it by writing a custom hook:
export function useAuthentication() {
const history = useHistory();
const user = useSelector(state => state.authentication.user);
const dispatch = useDispatch();
useEffect(() => {
if (!user) {
history.push(LOGIN);
});
return { user };
}
Which can then be called in my React components as follows:
const Dashboard = () => {
const { user } = useAuthentication();
return (
// My code
);
}