I am using React and Next.js and trying to redirect a user from a page when the data for that page is not available using Router.push('/another-page').
To do this I am checking for a status code in getInitalProps and applying a conditional. It looks like this:
const statusCode = action.currentArticle ? 200 : 404
if (isServer) res.statusCode = statusCode
if (statusCode === 404) {
Router.push('/')
}
The status code is being set properly and it makes it inside the conditional, at which point I am greeted with this error: No router instance found. You should only use "next/router" inside the client side of your app.
Actually, I am getting the same error no matter WHERE in the component's lifecycle events I try to redirect, and am getting little info online about this error.
The pattern of redirecting from getInitalProps can be seen in this next.js wiki: HERE
Any ideas on why this error is occurring or how to fix it are much appreciated ;)
With Next.js (and any universal react rendering) your code is executing in two different environments. First in Node (on the server) and then in a browser. Next does some work to provide unified functions that run in both these environments but they're very different. Next can't and doesn't keep this from you. It seems like you just loaded a page in your browser but here's a little more detail on what's really going onβ¦
On the client/browser:
Type url in the address bar (localhost:3000 or whatever), press enter.
GET request goes out to the server (Node).
On the server/Node:
GET request comes in.
Node gives you a request and a response object.
Maybe you have some Express routing/middleware.
At some point Next's render() function is called with the request and response objects.
Next runs getInitialProps and passes in the request/response.
React renderToString() is called which calls the following React lifecycle methods:
constructor()
componentWillMount()
render()
React creates a string of HTML that gets sent to the client.
^ This is Node. You can't access window, you don't have fetch, and you can't use the Next Router. Those are browser things.
Back on the client:
HTML is downloaded and rendering begins.
Links to js/css files in the HTML are downloaded/run.
This includes js code compiled by Next.
React render() is run which associates the downloaded HTML (the DOM) with a React virtual DOM. The following React lifecycle methods will run:
constructor()
componentWillMount()
render()
componentDidMount()
All other lifecycle methods (updates) will run when props/state change.
^ This is the browser. You have window, you have fetch, you can use the Next Router. Now you don't have the Node request/response but that seems to catch people up less.
Ref: Component lifecycle
The way works like #Shi said, but there is not server in getInitialProps. Instead of that, there should check window:
getInitialProps({res}){
if(typeof window === 'undefined')
res.redirect('/');
else
Router.push('/');
}
You can redirect from getInitialProps() like this:
import Router from 'next/router'
static getInitialProps = (ctx) => {
// On server
if(typeof window === 'undefined'){
res.writeHead(302, {location: '/dashboard'})
res.end()
} else {
// On client
Router.push('/dashboard')
}
return {}
}
See https://github.com/zeit/next.js/issues/649
next/router is not available on the server that's way you get an error saying that router not found, next/router can only be used on the client side.
For you to redirect a user inside getInitialProps in the server you can use:
getInitialProps({server,res}){
if(server)
res.redirect('/');
else
Router.push('/');
}
To make sure the page never render, we need to add await new Promise(() => {}) to end. The promise no needed resolve anything.
Home.getInitialProps = async ({res}) => {
if(res) {
res.writeHead(302, {location: '/dashboard'});
res.end();
} else {
// window.location.href = '/dashboard';
// Or with SPA redirect
Router.push('/dashboard');
}
await new Promise(() => {});
return {}
}
I found this https://www.npmjs.com/package/nextjs-redirect to be very simple and solved the issue for both client and server side.
pages/donate.js
import redirect from 'nextjs-redirect'
export default redirect('https://paypal.me')
Related
Context & Reproducible Scenario
I'm using the combination of these libraries and tools:
NextJS 12+ (based on React 18+)
MSAL-Browser 2.25+ and MSAL-React 1.6+ (Microsoft's libs for OpenID login against Azure B2C)
I'm using the Auth Code + PKCE redirect flow so this is the flow for users:
They land on /, the home page
They click a /me router link
They go to Azure B2C to log in because said page has this logic:
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
authenticationRequest={loginRequest}>
where loginRequest.state is set to router.asPath (the "intended" page: /me)
Note that the page is also wrapped in a <NoSsr> component based off Stack Overflow.
User logs in on Azure B2C, gets redirected back to my app at / (the root)
β Problem: the user now briefly sees the / (home) page
After a very brief moment, the user gets sent to /me where they are signed in
The MSAL docs don't seem to have much on the state property from OIDC or this redirect behavior, and I can't find much about this in the MSAL sample for NextJS either.
In short: the issue
How do I make sure MSAL-React in my NextJS application send users to the "intended" page immediately on startup, without briefly showing the root page where the Identity Server redirects to?
Relevant extra information
Here's my custom _app.js component, which seems relevant because it is a component that triggers handleRedirectPromise which causes the redirect to intended page:
export default function MyApp({ Component, pageProps }) {
return (
<MsalProvider instance={msalInstance}>
<PageHeader></PageHeader>
<Component {...pageProps} />
</MsalProvider>
);
}
PS. To help folks searching online find this question: the behavior is triggered by navigateToLoginRequestUrl: true (is the default) in the configuration. Setting it to false plainly disables sending the user to the intended page at all.
Attempted solutions with middleware
I figured based on how APP_INITIALIZERs work in Angular, to use middleware like this at some point:
// From another file:
// export const msalInstance = new PublicClientApplication(msalConfig);
export async function middleware(_request) {
const targetUrlAfterLoginRedirect = await msalInstance.handleRedirectPromise()
.then((result) => {
if (!!result && !!result.state) {
return result.state;
}
return null;
});
console.log('Found intended target before login flow: ', targetUrlAfterLoginRedirect);
// TODO: Send user to the intended page with router.
}
However, this logs on the server's console:
Found intended target before login flow: null
So it seems middleware is too early for msal-react to cope with? Shame, because middleware would've been perfect, to allow as much SSR for target pages as possible.
It's not an option to change the redirect URL on B2C's side, because I'll be constantly adding new routes to my app that need this behavior.
Note that I also tried to use middleware to just sniff out the state myself, but since the middleware runs on Node it won't have access to the hash fragment.
Animated GIF showing the flashing home page
Here's an animated gif that shows the /home page is briefly (200ms or so) shown before /me is properly opened. Warning, gif is a wee bit flashy so in a spoiler tag:
Attempted solution with custom NavigationClient
I've tried adding a custom NavigationClient to more closely mimic the nextjs sample from Microsoft's repository, like this:
import { NavigationClient } from "#azure/msal-browser";
// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/performance.md#how-to-configure-azuremsal-react-to-use-your-routers-navigate-function-for-client-side-navigation
export class CustomNavigationClient extends NavigationClient {
constructor(router) {
super();
this.router = router;
}
async navigateInternal(url, options) {
console.log('π Navigating Internal to', url);
const relativePath = url.replace(window.location.origin, "");
if (options.noHistory) {
this.router.replace(relativePath);
} else {
this.router.push(relativePath);
}
return false;
}
}
This did not solve the issue. The console.log is there allowing me to confirm this code is not run on the server, as the Node logs don't show it.
Attempted solution: go through MSAL's SSR docs
Another thing I've tried is going through the documentation claiming #azure/msal-react supports Server Side Rendering (SSR) but those docs nor the linked samples demonstrate how to solve my issue.
Attempted solution in _app.tsx
Another workaround I considered was to sniff out the hash fragment client side when the user returns to my app (and make sure the intended page is also in that state). I can successfully send the OpenID state to B2C like this...
const extendedAuthenticationRequest = {
...authenticationRequest,
state: `~path~${asPath}~path~`,
};
...and see it returned in the Network tab of the dev tools.
However, when I try to extract it in my _app.tsx still doesn't work. I tried this code from another Stack Overflow answer to get the .hash:
const [isMounted, setMounted] = useState(false);
useEffect(() => {
if (isMounted) {
console.log('====> saw the following hash', window.location.hash);
const matches = /~path~(.+)~path~/.exec(window.location.hash);
if (matches && matches.length > 0 && matches[1]) {
const targetUrlAfterOpenIdRedirect = decodeURIComponent(matches[1]);
console.log("Routing to", targetUrlAfterOpenIdRedirect);
router.replace(targetUrlAfterOpenIdRedirect);
}
} else {
setMounted(true);
}
}, [isMounted]);
if (!isMounted) return null;
// else: render <MsalProvider> and the intended page component
This does find the intended page from the state and executes routing, but still flashes the /home page before going to the intended page.
Footnote: related GitHub issue
Submitted an issue at MSAL's GitHub repository too.
I'm setting up a NextJS app using getInitialProps to pull misc data that I would like to be server-side-rendered on first page load. All is working fine, but I noticed that if I click the same link twice, getInitialProps runs again, even though everything is the same.
For example I have a left nav with links to various categories etc (this app is going to be a front-end for an ecommerce site). If I click to a category, the category component (page) loads just fine. Then if I click the exact same link, the getInitialProps of the category component runs again, causing the page to blank out for a second while the same data (the item list) is fetched and re-rendered.
So is there a way to prevent getInitialProps from running if the user clicks the same link twice?
Note that I'm using getInitialProps for two reasons:
getStaticProps is out because I don't plan to build the entire site at build time
getServerSideProps is usually out because I don't like that it ends up doing two http requests: first a request goes to the NextJS server, then the server sends a request to my API (which happens to live on a different server). I'd rather skip the middle man
Some code:
Category.getInitialProps = async (context) => {
let config = await import("../../config/config");
let response = await axios.get(`${config.default.apiEndpoint}&cAction=getCTGY&ctgyCode=${context.query.code}`);
let queryString = {...context.query};
if ( response ) {
return {
category: response.data,
queryString: queryString,
pathname: context.asPath
};
} else {
return {
category: null
}
}
};
You should Look into shallow routing which enables you to change the URL of the page without re-running the data fetching. This includes getInitialProps.
Shallow routing also works on <Link /> components.
<Link href="/" shallow>
<a>Home</a>
</Link>
You should be aware of the caveats though, they're documented here.
I am trying to create a Next JS application that handles the authentication and initial routing inside getInitialProps. I discovered this method can be executed either in the server or on the client.
My approach so far it's to have 2 different handlers based on detecting if I am in executing in the server checking for the presence of the req attribute inside of ctx.
This does the trick but doesn't feel like is the right way of doing. Can somebody, please, tell me if there is a cleaner way.
All authentication is handled in a separate subdomain, so I just need to redirect to the auth subdomain if there is no cookie or auth request fails for some other reason.
import "../../styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
MyApp.getInitialProps = async (appContext) => {
let cookie, user;
let ctx = appContext.ctx;
//Check if I am in the server.
if (ctx.req) {
cookie = ctx.req.headers.cookie
//Do auth request.
//Redirect base on user properties
// handle redirects using res object
ctx.res.writeHead(302, { Location: "/crear-cuenta"});
} else {
cookie = window.document.cookie;
//Do auth request.
//Redirect base on user properties
//Do redirects using client side methods (useRouter hook, location.replace)???
}
//Return pageProps to the page with the authenticted user information.
return { pageProps: { user: user } };
};
export default MyApp;
I think your code is clean enough. Of course you still can maintain it.
My suggestion would be as the followings:
MyApp.getInitialProps = async (appContext) => {
in this line you can use object destructuring technique to get the context straightforward:
MyApp.getInitialProps = async ({ ctx }) => {
then you won't need this line for example anymore : let ctx = appContext.ctx;
The most important part of your code which can be cleaned up by the way is the area that you have written your auth request twice in an if/else condition. I would suggest you to implement that part like this:
const cookie = ctx.req ? ctx.req.headers.cookie : window.document.cookie;
Although I would try to keep everything in getInitialProps on server side, In that case I make a small change to get the cookie as following and process it in server-side only.
const cookie = cookie.parse(ctx.req ? ctx.req.headers.cookie || "" : undefined);
Note that: I'm using a cookie parser which u can install the package yourself as well. (npm install cookie)
if you need to do an extra check on your cookie at client side, I will do that in componentdidmount or in case you are using react hooks in useEffect. But it is not necessary.
Now you can implement //Do auth request once, which will cause cleaner code and of course to reduce unnecessary repetition.
I have one quick and dirt question.
Is it possible to catch a query parameter from a server redirect inside of Gatsby.js application?
We have a Pardot tracking link that does redirect to our thank you page which is built in Gatsby.js and I want to pass some query parameters to the application it self from that redirect.
So for example:
www.trackedlink.com/thank-you?programme_code=CODE_FROM_REDIRECT_ON_SERVERSIDE
will redirect to:
www.gatsbyapplicationthatwillreadthequery.com/thank-you?programme_code=CODE_FROM_REDIRECT_ON_SERVERSIDE
Is it possible to read that query inside of the application if it's coming from the outside of the app?
Cheers and have a great week!
If they are triggered in the client-side the redirection will be caught by the application and yes, it would possible if they are coming from outside the app or using a standard anchor. Not using a #reach/router (<Link> component since it's a limitation).
A clean and scalable way to use it is by adding in the function in your gatsby-browser.js configuration:
import React from 'react';
import { checkUrlFunction } from './src/services/yourCheckUrlFunction';
export const onClientEntry = () => checkUrlFunction();
Adding a function in gatsby-browser.js with onClientEntry API will trigger your function once the page is loaded. From the documentation:
onClientEntry Function (_: emptyArg, pluginOptions: pluginOptions) => undefined Called when the Gatsby browser runtime first starts.
Your function should look like:
export const checkUrlFunction = () => {
if (typeof window !== 'undefined') {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const programmeCode= urlParams.get('programme_code')
if(programmeCode) window.localStorage.setItem('programmeCode', programmeCode)
console.log(programmeCode); // will output CODE_FROM_REDIRECT_ON_SERVERSIDE
};
};
Note the typeof window !== 'undefined' necessary to avoid issues if the window object is not defined when triggering the function
Hi Ferran, thank you for your solution but unfortunately, it does not
work when the redirect happens. It only works if the query string is
inside of the application
Yes, the idea of adding the function in gatsby-browser.js is to avoid the addition of checkUrlFunction() in each page, template, or component. The disadvantage is that you lose a bit of control but it saves a lot of overwriting code and improves the scalability and readability.
Thanks, Ferran, if you could show me an example of it - it would be
amazing! This cookie topic is sort of unknown water for me
So, with your specifications updated, I've added the localStorage approach since it's easier to achieve in a non-IDE environment like this, but the idea is exactly the same.
Set a vault (cookie or localStorage) automated in the gatsby-browser.js function
if(programmeCode) window.localStorage.setItem('programmeCode', programmeCode)
This sets a localStorage key/value pair ('programmeCode' (key)/programmeCode (value)
Access to that vault in your component. Use a componentDidMount lifecycle or useEffect hook to ensure that is loaded before the DOM tree is mounted.
useEffect(()=>{
if(typeof window !== undefined) console.log(window.location.getItem('programmeCode')
}, [])
I want to modify create-react-app service worker file and implement popup message which will ask user to update app if newer service worker is ready to be activated. I'm almost done with the solution but have one pitfall. I want to reload the app when user confirms service worker update popup, so I've added some coded to the end of register function, see below:
export default function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker."
)
})
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl, config)
}
let preventDevToolsReloadLoop
navigator.serviceWorker.addEventListener("controllerchange", function() {
// ensure refresh is called only once
if (preventDevToolsReloadLoop) {
return
}
preventDevToolsReloadLoop = true
console.log("reload")
window.location.reload(true)
})
})
}
}
But the problem is that it reloads the app also on first visit, when there doesn't exist any service worker yet. How can I solve it?
Update to react-scripts ^3.2.0. Verify that you have the new version of serviceWorker.ts or .js. The old one was called registerServiceWorker.ts and the register function did not accept a configuration object. Note that this solution only works well if you are Not lazy-loading.
then in index.tsx:
serviceWorker.register({
onUpdate: registration => {
alert('New version available! Ready to update?');
if (registration && registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
window.location.reload();
}
});
The latest version of the ServiceWorker.ts register()function accepts a config object with a callback function where we can handle upgrading. If we post a message SKIP_WAITING this tells the service worker to stop waiting and to go ahead and load the new content after the next refresh. In this example I am using a javascript alert to inform the user. Please replace this with a custom toast.
The reason this postMessage function works is because under the hood CRA is using workbox-webpack-plugin which includes a SKIP_WAITING listener.
More About Service Workers
good guide: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68
CRA issue discussing service worker cache: https://github.com/facebook/create-react-app/issues/5316
If you are not using CRA, you can use workbox directly: https://developers.google.com/web/tools/workbox
Completing #jfbloom22's answer:
As you probably want to ask the user after an update has been detected with something more complex than a plain alert, you need to ensure the registration object is available from inside the React's components tree and save it to use after the user accepts to update (for example, by clicking a button).
As an option, in a component you can create a custom event listener on a global object like document and fire this event when the onUpdate callback passed to serviceWorker.register(), passing to it the resgistration object as extra data.
This is exactly what my recently published Service Worker Updater does (some self-promotion). To use it you just need to:
Add it to the dependencies:
yarn add #3m1/service-worker-updater
Use it in your index.js:
import { onServiceWorkerUpdate } from '#3m1/service-worker-updater';
// ...
// There are some naming changes in newer Create React App versions
serviceWorkerRegistration.register({
onUpdate: onServiceWorkerUpdate
});
Use it in some of your React components:
import React from 'react';
import { withServiceWorkerUpdater } from '#3m1/service-worker-updater';
const Updater = (props) => {
const {newServiceWorkerDetected, onLoadNewServiceWorkerAccept} = props;
return newServiceWorkerDetected ? (
<>
New version detected.
<button onClick={ onLoadNewServiceWorkerAccept }>Update!</button>
</>
) : null; // If no update is available, render nothing
}
export default withServiceWorkerUpdater(Updater);
Try to add reload funtion in installingWorker.state === 'installed'
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// do something ..
}
}