This question already has an answer here:
How can I set a flash variable in Next.js before a redirect?
(1 answer)
Closed 9 months ago.
For example, assume I have a page called internal.tsx that has:
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getSession(ctx);
if (!session) {
// TODO: Add a toast notification explaining the redirect. Ideally, the desired destination should be remembered and should be redirected to after login.
return {
redirect: {
destination: '/',
permanent: false,
},
};
}
const props = ...
return { props };
};
If a visitor browses to this page at /internal, they will get bounced to my signin page without explanation.
I instead want my signin page to show a toast notification at the top of the screen explaining that the page they tried to access requires a login and that they will be returned to that page once they log in.
I plan to use a library like https://github.com/fkhadra/react-toastify on the signin page but haven't been able to figure out how / where to read from the session that the visitor had just been redirected.
Ideally each page (such as /internal) initiating a redirection could specify its own custom message (e.g. saved to a "flash variable" in the session) for the signin page to display in the toast.
Attempting an answer here based on the comment above.
Instead of using Redirect, it might be better to use the context and set a new header and do a context.res.redirect which now contains the new header flashVariable (borrowing from Laravel) from my previous comment - the inbuilt redirect does not seem to have the ability to set these values.
i.e
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getSession(ctx);
if (!session) {
ctx.res.setHeader("yourFlashVariable", "yourFlashValue");
ctx.res.redirect('/')
}
const props = ...
return { props };
};
The in your / route, you can look for the FlashVariable and invoke the toast library.
Is it possible to have protected routes in the Remix.run React framework, so that only admin users get the protected components, while regular users don't get the protected components at all as part of the JS bundle sent to the browser?
Also, this may require a form of code splitting on the front end side. Is code splitting supported in Remix.run?
this is a code snippet from a sample app I wrote, this is the home page and can only be accessed if the user is authenticated.
the redirect(`/login?${searchParams}`) will redirect if the user isn't authenticated
// Loaders provide data to components and are only ever called on the server, so
// you can connect to a database or run any server side code you want right next
// to the component that renders it.
// https://remix.run/api/conventions#loader
export let loader = async ({ request }) => {
const redirectTo = new URL(request.url).pathname;
let session = await getSession(request.headers.get("Cookie"));
// if there is no access token in the header then
// the user is not authenticated, go to login
if (!session.has("access_token")) {
let searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
throw redirect(`/login?${searchParams}`);
} else {
// otherwise execute the query for the page, but first get token
const { user, error: sessionErr } = await supabaseClient.auth.api.getUser(
session.get("access_token")
);
// if no error then get then set authenticated session
// to match the user associated with the access_token
if (!sessionErr) {
// activate the session with the auth_token
supabaseClient.auth.setAuth(session.get("access_token"));
// now query the data you want from supabase
const { data: chargers, error } = await supabaseClient
.from("chargers")
.select("*");
// return data and any potential errors alont with user
return { chargers, error, user };
} else {
return { error: sessionErr };
}
}
};
You can protect routes by authorizing the user inside the loader of the Route, there you could decide to redirect it somewhere else or send a flag as part of the loader data so the UI can hide/show components based on it.
For the code splitting, Remix does it at the route level, but it doesn't support server-side code-splitting out of the box, you may be able to support it with react-loadable.
I hope it has, but not. Below is the official answer.
https://remix.run/docs/en/v1/pages/faq#how-can-i-have-a-parent-route-loader-validate-the-user-and-protect-all-child-routes
You can't 😅. During a client side transition, to make your app as speedy as possible, Remix will call all of your loaders in parallel, in separate fetch requests. Each one of them needs to have its own authentication check.
This is probably not different than what you were doing before Remix, it might just be more obvious now. Outside of Remix, when you make multiple fetches to your "API Routes", each of those endpoints needs to validate the user session. In other words, Remix route loaders are their own "API Route" and must be treated as such.
We recommend you create a function that validates the user session that can be added to any routes that require it.
I use below libraries:
"#azure/msal-browser": "^2.16.0",
"#azure/msal-react": "^1.0.1",
I need to redirect the user to login screen without user being able to see any content. How would I be able to do that?
Currently, I have this:
export default function Main() {
const msalInstance = new PublicClientApplication(msalConfig);
useEffect(() => {
msalInstance
.handleRedirectPromise()
.then((s) => {
if (s === null) msalInstance.loginRedirect(loginRequest); // not logged in
else console.log(s); // logged in
})
.catch((a: any) => {
console.log("err");
console.log(a);
});
}, []);
return <App instance={msalInstance} />;
}
But, it loads the app for a second then does the redirection.
In order to achieve that specific behavior, there are two options:
Delay rendering your app until the user is confirmed to be logged in (they will be redirected to authenticate before you render your app).
Handle this on the server side by redirecting to the login interaction before the app is rendered (which would involve a solution other than msal-react).
Here is some documentation on the MSAL AuthenticationTemplate, which may be of some help in determining what the right way to use MSAL React would be in this case.
As a final note, it's not recommended that you initialize the PublicClientApplication inside a React component. Please review the Getting Started documentation and the react-router-sample for more guidance on how to initialize MSAL in your application.
Let's say I have a user's account information stored in localStorage (client side). I need my Next.JS app to render the webpage's navbar based on what's stored in localStorage (login or logout button). How can I first obtain the value from the client and then render the page? Or perhaps that isn't even what Next.JS is meant to do?
You can do something like this:
Use a variable in the state to prevent the page from being rendered
Use componentDidMount to load data from localStorage
When data is loaded, setState to allow component to be rendered.
It's a react issue, not a next.js issue.
You could use Conditional rendering for step 1.
Also read up on state here, and lastly componentDidMount.
Update:
Nowadays, I would opt for a React hooks implementation instead, but the idea still stands. useEffect can largely accomplish this with some nuances in some situations.
I also realize that there are some possible caveats with NextJS and SSR logic specifically, so this response may not be sufficient. In such cases, I would also look into some other responses below.
As mentioned at https://stackoverflow.com/a/54819843/895245 I haven't been able to truly get localStorage before the first render, only show a fallback page until that happens.
The fundamental issue is that Next.js maps one URL to one pre-render. And React hydration requires the initial server HTML to match the JavaScript structure:
React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them. In development mode, React warns about mismatches during hydration. There are no guarantees that attribute differences will be patched up in case of mismatches. This is important for performance reasons because in most apps, mismatches are rare, and so validating all markup would be prohibitively expensive.
That quote is not very clear if text-only changes work or not but the minimal test below shows that it raises a warning in that case, so you don't want to use it.
Therefore the only sure-fire way it to use useEffect to update the page afterwards.
However, when I've tested, the correct render with localStorage shows up so quickly that the intermediate one it is not noticeable at all, I'm not sure it even happens. The only problem is if you make different API calls on each case, see section "Differentiate between "not logged in" and "haven't decided yet" to avoid doing extra API calls" below for an example of that.
What I would like to do is to give a slightly more concrete idea about what was mentioned in that answer.
SWR example
Here is a complete runnable example where the navbar shows login status: https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app That repository is a fork of this one, both of which are Next.js implementations of the awesome realworld project.
The fallback in that case is just the signed out view of the blog pages, which already contain the key information users are likely to want to see, and can be cached e.g. with ISR.
That demo uses SWR to make the code slightly simpler. The key parts are:
navbar code
login code
The key parts of the code there are:
navbar:
import useSWR from "swr";
const Navbar = () => {
const { data: currentUser } = useSWR("user", key => {
const value = localStorage.getItem(key);
return !!value ? JSON.parse(value) : undefined;
});
login:
import { mutate } from "swr";
const LoginForm = () => {
const handleSubmit = async (e) => {
// Get `user` data structure from API.
mutate("user", data?.user);
We see that when the user logins, we call mutate on the "user" global identifier.
This redraws all components that contain that hook, which includes the navbar, as it setup the hook with the useSWR call.
This way, login first redraws the navbar, and then redirects you to home, so that the home page will have the redrawn navbar immediately. Without mutate, only the page body would redraw, not the navbar.
With this setup:
if you put a console.log(currentUser) just below useSWR, you see that it gets called twice.
So what happens is that it first returns immediately with a cached value (undefined) and the first render starts.
It then starts an async call to the cache, and when that returns, the hook triggers a re-render of the component, and the print happens again with the current user value.
This only happens on initial hydration during refresh/first hit. During internal page changes, there is just a single render.
All of this happens so fast that I can't see the page draw without hte local storage at all, not even with the Chromium debugger timeline frame inspection.
if we add a 2 second delay to the localStorage getter however:
const { data: currentUser } = useSWR("user", async (key) => {
await new Promise(resolve => setTimeout(resolve, 2000))
const value = localStorage.getItem(key);
return !!value ? JSON.parse(value) : undefined;
});
we do observe an intermediate page state with the user logged out, so it could in theory happen.
How it would look like without SWR
Of course, we wouldn't need to use SWR to achieve this.
The SWR documentation gives us the rationale of how thing would look like without SWR https://swr.vercel.app/getting-started to motivate their library.
You would either need to move the state up to a common parent of the login form + navbar, or you could use Context.
function Page () {
const [user, setUser] = useState(null)
// fetch data
useEffect(() => {
const value = localStorage.getItem(key);
const user = !!value ? JSON.parse(value) : undefined;
setUser(user)
}, [])
// global loading state
if (!user) return <Spinner/>
return <div>
<Navbar user={user} />
<Content user={user} />
</div>
}
// child components
function Navbar ({ user }) {
return <div>
...
<Avatar user={user} />
</div>
}
function Content ({ user }) {
return <h1>Welcome back, {user.name}</h1>
}
function Avatar ({ user }) {
return <img src={user.avatar} alt={user.name} />
}
As mentioned at What is difference between lifecycle method and useEffect hook? useEffect is the hook analogue to componentDidMount.
Checking typeof localStorage === 'undefined' leads to a warning
React doesn't like that and warns with something like:
Expected server HTML to contain a matching"
as it notices the difference between hydrated and non-hydrated pages: React 16: Warning: Expected server HTML to contain a matching <div> in <body>
Tested on Next.js 10.2.2.
Minimal reproducible example
Just to play with and see exactly what happens:
pages/index.js
import Link from 'next/link'
import React from 'react'
export default function IndexPage() {
console.error('IndexPage');
let [n, setN] = React.useState(0)
if (typeof localStorage === 'undefined') {
n = '0'
} else {
n = parseInt(localStorage.getItem('n') || '0', 10)
}
return <>
<Link href="/notindex">notindex</Link>
<div
onClick={() => {
localStorage.setItem('n', n + 1)
setN(n + 1)
}}
>increment</div>
<div
onClick={() => {
localStorage.removeItem('n')
setN(0)
}}
>reset</div>
<div>{n}</div>
</>
}
pages/notindex.js
import Link from 'next/link'
export default function NotIndexPage() {
return <Link href="/">index</Link>
}
package.json
{
"name": "test",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "12.0.7",
"react": "17.0.2",
"react-dom": "17.0.2"
}
}
Run:
npm install
npm run dev
Now, if you:
open /
increment
refresh the page
react gives a warning because it notices that the 0 text was changed to 1:
Warning: Text content did not match. Server: "0" Client: "1"
If we click the internal links however to notindex and back, we don't see the warning. This is because hydration is only done on the initial page refresh, further changes are done in Js only.
What we have to do instead is something like this:
import Link from 'next/link'
import React from 'react'
export default function IndexPage() {
console.error('IndexPage');
let [n, setN] = React.useState(0)
React.useEffect(() => {
console.error('useEffect');
setN(parseInt(localStorage.getItem('n') || '0', 10))
}, [])
return <>
<Link href="/notindex">notindex</Link>
<div
onClick={() => {
setN(n + 1)
localStorage.setItem('n', n + 1)
}}
>increment</div>
<div
onClick={() => {
localStorage.removeItem('n')
setN(0)
}}
>reset</div>
<div>{n}</div>
</>
}
Differentiate between "not logged in" and "haven't decided yet" to avoid doing extra API calls
OK, I had another issue: I was making unnecessary API calls, because first the page thought the user was logged out, and then it thought it was logged in, and each of those needed to do different API calls.
Unlike starting to render the wrong page, this would actually have server load consequences, so it was not acceptable.
The solution I used was to differentiate between:
undefined: haven't decided
null: not logged-in
and not make any requests on undefined.
Here's a non-minimized demo:
https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app/blob/2bbce5199d3a7efa19a3a58426bea25a1cd37579/front/ArticleList.tsx#L33
https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app/blob/2bbce5199d3a7efa19a3a58426bea25a1cd37579/front/useLoggedInUser.ts
I'll try to minimize it later on.
Another solution: just do SSR
In general, SSR is way simpler than ISR, because you don't have to worry about this get page/ask for data/get data/update page dance from Hell.
ISR is an optimization, and you should only use if there's a proven performance benefit.
Remember that SSR in Next.js is also very data efficient, as Next.js returns only the .json from getServerSideProps on page switches, basically exactly like an API would.
You can then just do authentication from getServerSideProps with cookies, and return the correct page straightaway.
This is how I did it.
const setSession = (accessToken) => {
if (typeof window !== 'undefined')
localStorage.setItem('accessToken', accessToken);
};
const getAccessToken = () => {
if (typeof window !== 'undefined')
return localStorage.getItem('accessToken');
};
Here is where I call them to handle login and to get the access token:
const loginWithEmailAndPassword = async (email, password) => {
const { data } = await axios.post(`${apiUrl}/login`, { email, password });
const { user, accessToken } = data;
if (user) {
setSession(accessToken);
return user;
}
};
const accessToken = getAccessToken();
local storage is not available on the server, there are two options to resolve this
1: create HOC or custom hook to check if the local storage has the data (this is normal react way)
2: you can use cookies to store data on client and server side , which can be then be used getServerSideProps to extract the data and and you can then use this data to display the information accordingly on the initial render.
you can use useEffect hook and useState, so that when component loads, useEffect will fire last, extract data from localStorage and assign it to a STATE from useState.
then you can access your data from useState, states. if that makes sense.
Bottom line, useEffect allows to easily extract data from localStorage, so then you can do what you like with it.
const [userData, setUserData] = useState({});
console.log(userData);
useEffect(()=> {
setUserData(localStorage.getItem('userSession'));
}, [])
The first render which happen on server side can not have access to localStorage and throw the error. To prevent this, add an extra layer of defense with
if (typeof window !== 'undefined') {
// run logic that read/write localStorage
}
Then it should skip the logic when on server and run it when loaded on client side
I use Firebase with VueJS. Sign-up, sign-in works fine so far but as soon as I reload the page with F5 or load a page with writing to the browsers address bar directly then the auth session is killed, the user isn't signed-in anymore. It works fine as long as I don't reload the page, even if I click route links or get redirected to other pages, the user session keeps alive. It doesn't matter where I reload the page btw.
My login method:
firebase.auth().signInWithEmailAndPassword(this.username, this.password).then(
(user) => {
if (user.emailVerified === false) {
firebase.auth().signOut()
} else {
// redirect to lobby page if user is verified and signed in
this.$router.push('/lobby')
}
},
function(err) {
console.log(err.message)
}
)
You need to call firebase.auth().onAuthStateChanged in order to detect whether a user is logged in.
This method gets invoked in the UI thread on changes in the authentication state:
Right after the listener has been registered
When a user is signed in
When the current user is signed out
When the current user changes
Source
Example usage can be like this:
firebase.auth().onAuthStateChanged(user => {
if (user) {
if (user.emailVerified === false) {
firebase.auth().signOut()
} else {
this.$router.push('/lobby')
}
} else {
// will get to here from a logout event
// this.$router.push('/login')
}
}
Use this in your main.js file:
firebase.auth().onAuthStateChanged(() => {
if (!app) {
app = createApp(App).use(store).use(router).mount('#app')
}})
Firebase will run before Vue app is created.