Next.js client side protected routes without server side rendering - reactjs

I am a little bit confused about protected routes in next.js.
First of all, I don't want to use any server side rendering. I want to statically export
via next export. In that case, how do I implement client side protected routes?
Say, I have a back-end server with basic JWT authentication. How do I make certain routes protected from certain user and redirect them in /login page?

Since you're wanting to create protected routes with a static export, you'll need to do everything in the browser.
Verify their JWT in the browser
If their JWT is valid, render the page (including all requests to fetch data)
If their JWT is not valid, redirect them
For this, we're going to create a wrapper AuthCheck component.
Related:
How can you create a Private route in next.js?
AuthCheck
For verifying the JWT, you can use any method you'd like, including sending it to an api endpoint to verify it. Though I'm unsure if you can use Next.js api endpoints with static exports.
import { useRouter } from 'next/router'
export const AuthCheck = (props) => {
const router = useRouter()
const isJWTValid = useIsJWTValid() // you need to implement this. In this example, undefined means things are still loading, null means user is not signed in, anything truthy means they're signed in
if (typeof window !== 'undefined' && user === null) router.push('/')
if(!user) return <Loading /> // a loading component that prevents the page from rendering
return props.children
}
You can then add this to your app.js.
const MyApp = ({ Component, pageProps }) => {
return (
<AuthCheck>
<Component {...pageProps} />
</AuthCheck>
)
}
export default MyApp
Alternatively, you can also add this to any individual page. With this, you may need to debug the timing of any fetched data.
export default const ProtectedPage = () => {
return (
<AuthCheck>
<!-- contents of the page -->
</AuthCheck>
)
}

Related

Navigating From One local Host to another in react js

How can i navigate from one localHost:3000 to localHost:3001 in react Js
currently my auth is running in localHost:3000 and i want to navigate it to localHost:3001 when login successful
my current implementation is like
{agency && <Navigate to={"http://localhost:3001/"} replace />}
but it navigate me to http://localhost:3000/Login/localhost:3001/
but i want http://localhost:3001
Anyone?
react-router-dom only handles internal navigation actions, i.e. it can only link to and route within the React application. "http://localhost:3001/" is interpreted as an internal path and is appended to the current path.
If you need to navigate to an external (to the app) URL you should use raw anchor (<a />) tags or issue an imperative redirect via the window.location object.
Create a new navigation component to handle issuing the side-effect of external redirect in an useEffect hook.
Example:
const NavigateExternal = ({ to }) => {
useEffect(() => {
window.location.href = to;
}, []);
return null;
};
...
{agency && <NavigateExternal to="http://localhost:3001/" />}
You'd need to use
window.location.href = 'http://localhost:3001/'
Or using good practices
const redirectToExternalResource = url => window.location.href = URL
redirectToExternalResource('http://localhost:3001/')

Next js how to fetch localStorage data before client side rendering

I am using react + next.js in my client side and I am getting this weird warning, which I've failed to fix over the previous 3 days.
I assume the Warning arise in because the isUserAuthenticated value is saved on the localStorage.
and localStorage is only available on the client side, and therefore the value is diffrent from the initial data rendered on the server.
after searching on google I read a few post which suggested using componentDidMount() to in order to load the localStorage values before the the client side in rendred.
unfortunelty I've failed to implement this idea, threfore I am asking for help, can someone help me to to solve this problem? thanks in advance
The warning:
react-dom.development.js:67 Warning: Text content did not match.
Server: "false" Client: "true"
at div
at ul
at div
at nav
at Navbar
Navbar.js
const Navbar = () => {
const { isUserAuthenticated, } = useSelector((state) => state.authReducer);
return (
<nav data-testid='navbar'>
<div>
<>{isUserAuthenticated ? <div>true</div> : <div>false</div>}</>
</div>
</nav>
);
};
export default Navbar;
the solution
// TODO recipes crud to navbar
// test author and guest links
import { useSelector } from 'react-redux';
import React from 'react';
const Navbar = () => {
const [isUserAuthenticated, setIsUserAuthenticated] = useState();
useEffect(() => {
setIsUserAuthenticated(store.getState().authReducer.isUserAuthenticated);
}, []);
return (
<nav data-testid='navbar'>
<div>
<>{isUserAuthenticated ? <div>true</div> : <div>false</div>}</>
</div>
</nav>
);
};
export default Navbar;
This error happens when your server returns a different html structure than your client side, this will force React to re-render the entire App instead just attach event listeners (which is much faster).
From the error, looks like your isUserAuthenticated & loggedUserData have different values at client side & server side, Print them, if this is the situation, check the reason for this.
Edit
As you've mentioned, localStorage available only at client side, this means that your code most support it.
At the serverSide (there is no localStorage) your isUserAuthenticated should return false.
At the client side, you should "continue" the server state (which means that you don't load the value from localStorage firstly), then when the app is mounted, load the value.
if you are using classes, use the componentDidMount (which runs only at client side) in order to update the value of isUserAuthenticated.
if you are using hooks, use the useEffect hook (which runs only at client side) in order to update the value of isUserAuthenticated.

Redux vs HOC in React app with auth session

I have React app in framework Next.js (but framework is not important) with authentification implemented.
Current app is getting user session data by API call after first load (I am using next-auth for this). This causes that there is interval between load and session fetch which results in small interval where user see loading spinners all over app.
I want to mitigate this with implementation of SSR (Server-side-rendering) where I can hydrate initial response with session data.
This can be easily done in Next.js by getServerProps method in Component, but the problem is that I need to implement this method on every single page I have.
Thats why I am searching for a way to make session state global across whole app so every page (component) can access this session data.
My main question is, if I should use HOC wrapper where I will implement getServerProps to receive session data, or I should use Redux store and hydrate this store with by same method.
I am not exactly sure if HOCs are still widely used, or they are just deprecated now.
I do not want to implement getInitialProps in Next.js custom _app.js file because this will disable ability to have statically genererated pages and session will be available in every page. I am planning that app needs this session data only in about half of pages.
So should I go with HOC or just inplement redux store for this?
You could do the following:
Create an authentication Context Provider that will pass auth data down to your components. As an example:
interface ContextProps {
authenticated: boolean;
handleLogin: () => void;
handleLogout: () => void;
}
const authContext = createContext<Partial<ContextProps>>(undefined);
Along with Context, create a hook that will make use of useEffect to determine authenticated state - could be checking cookie exists, or await call to API to check auth status. Set this as the relevant status and use this value in your provider.
interface AuthProviderProps {
children: any;
}
const AuthProvider = (({ children }: AuthProviderProps) => {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
});
export const useAuth = () => useContext(authContext);
export default AuthProvider;
Wrap in your _app.js:
...
<AuthProvider>
<Component pageProps={pageProps} />
</AuthProvider>
...
Access in relevant pages:
const Page = () => {
const { authenticated } = useAuth();
if (!authenticated) {
return (
<>
Not authenticated
</>
);
}
...
Then you can consume in your relevant components

Can I use getState method from redux store to implement a utility function for my application?

I have a working react-redux application which does login functionality and redirects users to their respective pages based on their roles. In order to check their roles, I'm using jwt provided by the auth reducer and decoding it in the login component itself (which I think has extractable logic) and redirecting them. I want to do something like this -->
//checkAuthAndRole.js -- utility
const {isAuthenticated, token} = store.getState();
function checkAdmin() {
if(!isAuthenticated) {
return <Redirect to='/login' />
} else //decode token here if role === admin redirect them to admin page
}
// export this utility and call it in respective component
Can I do this?
Using state in utility would not be a right design. 'isAuthenticated' and 'token' can be passed to utility which can then decide the page to load

is there a faster way to check the user's state in firebase like onAuthStateChanged?

I have currently working on a next.js project using react, redux and firebase.
When user enters a page that need authorization I use the following code to redirect them if they are not authenticated.
import React from 'react';
import Router from 'next/router';
import { firebase } from '../../firebase';
import * as routes from '../../constants/routes';
const withAuthorization = (needsAuthorization) => (Component) => {
class WithAuthorization extends React.Component {
componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
if (!authUser && needsAuthorization) {
Router.push(routes.SIGN_IN)
}
});
}
render() {
return (
<Component { ...this.props } />
);
}
}
return WithAuthorization;
}
export default withAuthorization;
I am using the following repo as my base project. The problem is the app seems to work fine but when I try to navigate to a page that requires authentication when I am not authenticated. I am not redirected immediately rather it shows the page first then redirects. Since the HOC uses
firebase.auth.onAuthStateChanged()
which is asynchronous. Is there a faster way of checking if the user is logged in.
I have so far considered the
firebase.auth().currentUser
but my internal state depends on the update of the onAuthStateChanged function when the user logs out in another page.
Using a listener to onAuthStateChanged is usually the correct way to restore authentication state upon a full page transition. Since there is a reload happening at such a transition, there is a chance that the authentication state has changed. Using an onAuthStateChanged listener ensures that Firebase calls your code after it has validated the authentication state of the user. Since this may require a call to the server, this can indeed takes some time, so you'll want to show a "loading..." animation.
If the state transition doesn't require a reload, like when you're just rendering a new route in the same page, then you can be reasonably certain that the authentication state doesn't change on the transition. In that case you could pass the current user from the previous component into the new one. Tyler has a good article on this: Pass props to a component rendered by React Router.
In the latter case firebase.auth().currentUser should also retain its state, and you can use it safely. You'd then still use an onAuthStateChanged to detect changes in authentication state after the new route has loaded.

Resources