I'm using nextjs 12.2.5 have created a higher order component in order to limit access to users that are logged in for certain pages but I keep getting hydration error.
Here is my code:
WithAuth.jsx
import { useRouter } from "next/router";
const withAuth = (WrappedComponent) => {
return (props) => {
if (typeof window !== "undefined") {
const Router = useRouter();
const token = localStorage.getItem("token");
if (!token) {
Router.replace("/login");
return null;
}
return <WrappedComponent token={token} {...props} />;
}
// If we are on server, return null
return null;
};
};
export default withAuth;
Home.jsx
const Home = () => {
return (
<Layout>
Home
</Layout>
);
};
export default withAuth(Home);
I have read the documentation on hydration but I still don't understand how I can implement my private route with Higher Order Component and avoiding the hydration error.
How am I supposed to implement private route with next js?
Related
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'.
I'm using next-cookies to store auth credentials from user session, however, I can't get them during app initialization -let's say, the user refreshes the page or comes back later-, however, after app has been initialized -or user has loged in-, I get them navigating in the app.
This is important, because I want to fetch some initial data to be available in to the redux store from the beginning.
// pages/_app.js
import { useStore } from '../store/store';
import nextCookie from 'next-cookies';
function MyApp({ Component, pageProps }) {
const Layout = Component.layout || MainLayout;
const store = useStore(pageProps.initialReduxState); // custom useStore method to init store
const pageTitle = Component.title || pageProps.title || 'Título de la página';
return (
<Provider store={store}>
<Layout pageTitle={pageTitle}>
<Component {...pageProps} />
</Layout>
</Provider>
);
}
export default MyApp;
MyApp.getInitialProps = async (ctx) => {
const { token, user } = nextCookie(ctx);
/* TO DO
the idea is to get cookie from the server side
and pass it to the client side, if the cookie is
active, initial data will be triggered from the
initializers
*/
return { pageProps: { token, user } };
};
Check it out:
Is it a better way or native option to get the cookie without having to a different cookie dependency?
Btw, I already have a middleware that protects routes from non authenticated user which works fine:
// pages/_middleware.js
export function middleware(req) {
const activeSession = req.headers.get('cookie');
const url = req.nextUrl.clone();
if (activeSession) {
if (req.nextUrl.pathname === '/login') {
url.pathname = '/';
return NextResponse.redirect(url);
}
return NextResponse.next();
}
url.pathname = '/login';
return NextResponse.rewrite(url);
}
I have a functional component Profile, I only want users to access it if they are authenticated, if not, they should be redirected.
import React from 'react';
import {useAuth0} from '#auth0/auth0-react';
export const Profile = ({history}) => {
const {user, isAuthenticated} = useAuth0();
if (!isAuthenticated) history.push('/');
const {email, picture} = user;
return (
<div>
<h4>Profile</h4>
<p>{email}</p>
<p>{picture}</p>
</div>
);
};
Here I get an error if I try to access the /profile directly.
`TypeError: Cannot destructure property 'email' of 'user' as it is` undefined.
What I would like to do is the following:
render the Home component if the user is not authenticated and pass a props boolean.
redirect the app to '/'
I am trying to combine history.push('/') and return <Home message={true}/> but this doesn't work since the props is not being passed.
Is there a way to combine both? Or am I missing some extra steps?
The code after history.push('/') is causing error as the isauthenticated is false then user object does not contain those fields and hence the error!
import React from 'react';
import {useAuth0} from '#auth0/auth0-react';
export const Profile = ({history}) => {
const {user, isAuthenticated} = useAuth0();
if(!isAuthenticated) {
history.push('/');
return
}
else{
const {email, picture} = user;
return (
<div>
<h4>Profile</h4>
<p>{email}</p>
<p>{picture}</p>
</div>
);
}
};
This should work!!
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.
I want to redirect my user to any url that he types in after logging in
for example; my user types in the browser, http://localhost:3000/login/tickets,
If he has not logged in I would need the program to load the login page, and after login, the program redirects to this page, I can do it with a single page but I wanted it to be dynamic, something like this.
isAuthenticated()
? (
<Component {...props} />
) : <Redirect to=`/login?next=${this.props.location.search}` />
)}
soon this redirect would load the login page with the tag next
My solution was to do essentially what you describe. I made a HOC to wrap my route's component in if it requires that the user be logged in:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter, Redirect } from 'react-router-dom';
/**
* Higher-order component (HOC) to wrap restricted pages
*/
export default function LoggedInOnly(BaseComponent) {
class Restricted extends Component {
state = {};
static getDerivedStateFromProps(nextProps) {
const { history, location } = nextProps;
if (!nextProps.isLoggedIn) {
history.replace({ pathname: '/signin', search: `dest=${encodeURIComponent(location.pathname)}` });
}
return null;
}
render() {
const { location, staticContext } = this.props;
if (this.props.isLoggedIn) return <BaseComponent {...this.props} />;
const destinationURL = `/signin?dest=${encodeURIComponent(location.pathname)}`;
if (staticContext) staticContext.url = destinationURL;
return <Redirect to={destinationURL} />;
}
}
const mapStateToProps = state => ({
isLoggedIn: !!state.globalUserState.loggedInUserEmail,
});
return withRouter(connect(mapStateToProps)(Restricted));
}
I also set the url on the static context in my case so I can handle redirects appropriately in server side rendering. If you're not doing the same you can ignore that part.
For using it, though, I redirect after my SSR render function, like so:
if (context.url) {
console.log(`Redirecting to URL ${context.url}`);
return res.redirect(context.url);
}
A route using this would look like:
<Route path="/preferences" component={LoggedInOnly(SettingsView)} />
On my login page I grab the url parameters to see if there's a destination. If there is, I redirect there on login success.
I do this using query-string and the search component of the location:
const { destination } = queryString.parse(props.location.search);
The above assumes you're using withRouter to get the location info in the props.
On authentication success in the client I simply redirect to destination if it exists:
window.location.href = this.props.destination;
You could also use history.push or similar to accomplish the above.
In my case, as you can see, I'm using redux to track the logged in user state.
you need to update state by taking a variable and apply check wether it has been changed or not, if yes then redirect to desired page ,if not revert back. Since you have not posted your whole code .You can refer to this video for wider and clear perspective :
https://www.youtube.com/watch?v=zSt5G3s3OJI
You can accomplish what you need by doing some thing like this:
if(isAuthenticated)
this.props.history.push('/login', {lastPage: this.props.location.match})
and after user gets logged in you cant redirect him to passed param lastPage!
Another way is to store lastPage in redux and access it after user get logged in.
I had the same problem, i made a HOC to solve it.
import React from "react";
import { connect } from "react-redux";
import Login from "../../Auth/Login";
import { withRouter } from "react-router-dom";
import qs from "querystring";
const signInPath = "/signin";
const signUpPath = "/signup";
const forgotPassPath = "/forgot";
const resetPassPath = "/resetpassword";
const returlUrlPath = "returnUrl";
const allowedPaths = pathname =>
pathname === signInPath ||
pathname === signUpPath ||
pathname === forgotPassPath ||
pathname === resetPassPath;
const homePath = "/";
export default Component => {
class AuthComponent extends React.Component {
componentDidMount() {
this.checkAuthentication();
}
componentDidUpdate(nextProps) {
if (
nextProps.location.pathname !== this.props.location.pathname ||
this.props.loggedIn !== nextProps.loggedIn
) {
this.checkAuthentication();
}
}
checkAuthentication() {
const {
loggedIn,
history,
location: { pathname, search }
} = this.props;
if (!loggedIn) {
if (!allowedPaths(pathname)) {
const returlUrl =
pathname.length > 1
? `${returlUrlPath}=${pathname.replace("/", "")}`
: undefined;
history.replace({ pathname: signInPath, search: returlUrl });
}
} else if (search) {
const parsedSearch = qs.parse(search.replace("?", ""));
if (parsedSearch.returnUrl) {
history.replace({ pathname: parsedSearch.returnUrl });
} else {
history.replace({ pathname: homePath });
}
} else if (
history.location.pathname === signInPath ||
history.location.pathname === signUpPath
) {
history.replace({ pathname: homePath });
}
}
shouldRedirectToLogin() {
const {
location: { pathname }
} = this.props;
return (
!this.props.loggedIn &&
pathname !== signUpPath &&
pathname !== forgotPassPath &&
pathname !== resetPassPath
);
}
render() {
return this.shouldRedirectToLogin() ? (
<Login></Login>
) : (
<Component {...this.props}></Component>
);
}
}
return withRouter(
connect(({ user: { loggedIn } }) => {
return {
loggedIn
};
})(AuthComponent)
);
};
Thanks for all, after a lot of research i get only:
const params = (this.props.children.props.computedMatch.url);
return <Redirect to={`/login/?next=${params}`} />;