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.
Related
I am developing a ecommerce store using NEXT.JS and Redux. So in product listing page, I have sorting select dropdown with Price Low to High, Price High to Low and New Arrivals. Upon selecting this option, I want to change the URL without page refresh and API call should occure. I have tried using below code, but it is not working and page is reloading.
function sortBy(value) {
router.replace({
pathname: '/products/'+slug,
query: { sort: value }
})
dispatch(fetchproducts(slug, sort));
}
The above code just refresh the current page and appending sort param to URL.
So is it possible to do it without page refresh like in Flipkart.
With the help of shallow-routing change of URL without doing a page reload is possible. It can be enabled by passing explicit option object as third argument to Router.push, i.e { shallow: true }
From the docs
Shallow routing allows you to change the URL without running data fetching methods again, that includes getServerSideProps, getStaticProps, and getInitialProps.
You'll receive the updated pathname and the query via the router object (added by useRouter or withRouter), without losing state.
For example, this how you would update the query param sortBy for pathname /products with the help of shallow-routing.
Router.push({
pathname: '/products',
query: { sortBy: 'price' }
},
undefined, { shallow: true }
)
But there are a few caveats It is not possible to do shallow-routing between different pages, it works only for same page URL changes. See the caveat section for more details.
For example, you can update a query param for page /product page, but it won't be possible if you try to do shallow-routing from /product to /product/[slug] because they are two distinct pages.
// page will reload because shallow-routing not possible between the pages
Router.push('/product', '/product/some-product?sortBy=price', { shallow: true })
Example:
you have a folder like: posts/[id].js and your url is like http://something.com/posts/123
You want to add a query param that will not refresh the page and your url will be like: http://something.com/posts/123?param=ok
all you need to do is:
const postId = 123;
const param = 'ok';
router.push(
{
pathname: `/posts/[id]`,
query: {
postId,
param
}
},
`/posts/${postId}?param=${param}`,
{shallow: true}
);
I'm developing a Nuxt SSR app with a simple authentication.
I have auth middleware to guard all login required routes.
export default defineNuxtRouteMiddleware(async (to, from) => {
if (process.client) {
const authStore = useAuthStore()
if (!authStore.check) {
authStore.returnUrl = to.fullPath
useRouter().push('/admin/login')
}
}
})
This middleware checks browser cookie in store, hence it needs to be run on the client side
export const useAuthStore = defineStore('auth', () => {
const token = ref(useStorage('token', null))
const check = computed(() => token.value !== undefined)
....
From my understanding, normally the SSR middleware runs on the server side first and then the client side.
The problem is, when I apply this auth miidleware to gaurd a login required page
<script setup lang="ts">
definePageMeta({
middleware: ['admin-auth'],
// or middleware: 'auth'
})
</script>
<template>
<div class="flex justify-center items-center h-screen p-3">admin 1</div>
</template>
The middleware will run on the sever side first causing the page to render unintentionally, and then trigger the client side with the logic, and it will redirect back to login page. This is very ulgy. You can see it in action.
Has anyone run into this problem before? Any solution would be really appreiated. My requirement is to use SSR for this app.
Plus, another small problem. when running SSR and you refresh the page, there's some style fickering. I'm not sure why. I'm using this template https://github.com/sfxcode/nuxt3-primevue-starter
I've been looking for a solution for several days already #_#
In general, it is not necessary to use "SSR" for protected pages, because only public pages need to be indexed for search engines.
In SSR mode, you have access to the data stored in cookies. To get them, it is most convenient to use special libraries for working with cookies, so as not to prescribe all possible cases when you are either in SRR or CSR.
For Nuxt 2, I use the cookie-universal-nuxt library.
Try to make sure that the DOM tree does not differ on the server and the client, otherwise errors may occur.
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 am using navigate to move to another URL. I saw many posts using Link to move to another page with dynamic url. But I want to change url without writing jsx
When I navigate to the following url, I get a 404 error
navigate(`/vidx/${u}`, {
state: { vid: r }
})
I changed gatsby-node.js to following, still getting the same error. I have a file named vidx.js in pages folder
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path.match(/^\/vidx/)) {
page.matchPath = "/vidx/*";
createPage(page);
}
}
My url will look like this - www.xyz.com/vidx/123456789. The number 123456789 will depend upon the user logged in
I want to redirect to vidx.js; but the URL should be /vidx/123456789
That will never work since /vidx/123456789 will always throw a 404 error since it's not generated and doesn't exist, it's a separate new page.
One easy thing you can do use: /vidx?queryParameter=123456789. In that case, your page will remain being /vidx and you can get the queryParameter to make your stuff with your own logic.
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')