How do I break the infinite redirect loop I've created in Next JS? - reactjs

This application is being hosted on S3, entirely static (using next export), and we're routing all 404 errors to index.html in order to let those be handled on the client side so we can take advantage of dynamic routing. To handle this, I have the following in my _app.tsx file:
const { asPath, pathname, ...router } = useRouter();
// check if redirect
React.useEffect(() => {
if (pathname === '/' && asPath !== pathname) {
router.replace(asPath, undefined, { shallow: true });
}
}, [asPath]);
This works, for the dynamic routing aspect, but it introduces a new bug: when I navigate to a page that actually doesn't exist, like /fffff, there's an infinite loop of the app trying to reroute to /fffff. Ideally, it would only try to reroute once, and then default to the 404.tsx or _error.tsx page. I've tried creating a stateful boolean like hasRedirected and then just marking that as true in the useEffect, but that didn't work because the page is actually refreshing and thus resetting state on each router.replace call. How do I handle this error and break out of the loop?
update: The issue seems to be that when I call router.replace, Next doesn't find a path to match /fffff, so its default behavior is to try asking the server for the route by refreshing. I need to disable or intercept this behavior somehow.

The solution we ended up finding, which works quite well, uses session storage to store a hasRedirected variable that gets deleted after being read. Here's the code:
React.useEffect(() => {
if (router.isReady) {
const isRedirect = pathname === '/' && asPath !== pathname;
if (sessionStorage.getItem('hasRedirected')) {
sessionStorage.removeItem('hasRedirected');
if (isRedirect) router.replace('/404');
} else if (isRedirect) {
sessionStorage.setItem('hasRedirected', 'true');
router.replace(asPath);
}
}
}, [asPath, pathname, router.isReady]);

Related

Next.js How to make a redirect correctly

I have such a page structure.
locale can be any value from the language-country pair
eg en-ca, fr-fr, ar-en
[locale] // main folder route
page1.tsx // child route
page2.tsx // child route
page3.tsx // child route
For example, if I go to the address /page1, then I redirect to /locale/page1
But the problem is that I check on the browser side and first a 404 page appears in the browser and only then redirects to the correct address.
I think that it is necessary to check on the server.
I was making my own file for the server. But the problem is that there is no way to track the address for which the request is going and that means I cannot check if the address starts with the parameter I need.
In case you do not wont to use Next.js native i18n routing you can create fallback page file pages/[...fallback].ts to catch all non-existing page. Then you can use use getServerSideProps to redirect properly.
This could work for you (not tested):
// The page content will never be rendered
const FallbackPage = () => <div>Redirecting...</div>
// Is processed on every request on server side
export const getServerSideProps: GetServerSideProps = async (ctx) => {
// TODO: add custom detection (e.g. based on headers or cookies)
const locale = 'en'
const destination = `/${locale}/${ctx.params.fallback.join('/')}`
return {
props: {},
redirect: { destination, permanent: false },
}
}
export default FallbackPage

Programmatically navigate to a dynamic url with gatsby

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.

Next JS - Handling getInitialProps on _app.js in SSR vs CSR

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.

How to resolve an url by fetch & redirect without refresh with react

I need to fetch the current url in my react application because our front/nginx may respond with a redirect which won't be hit if user has the service-worker active..
therefor I currently have this logic in my 404 component on didMount.
fetch('/test/page/xxxx/', {redirect: 'follow'}).then(res => {
if(res.status === 200 && res.redirected) {
console.log(res)
// this.props.push(res.url)
// window.location.replace(res.url)
// window.location.hash = res.url
console.log('Redirected to ' + res.url)
}
})
the res.url that I get back in the respons is a full url ex: https://example.net/xxx/xxx/xxxx which makes it hard for me to use push from react-router-redux because it expects a relative url. Can anyone help me with a regex that can get the slugs from the res.url or does anyone have any other idea how to solve this problem?
There is the URL interface [1] available in the window that can be used create a URL object.
A URL object has the pathname property that is used to retrieve the path component of the URL.
this.props.push(
new URL(res.url).pathname
)
The easiest way to get the pathname (relative URL) is by parsing it with the URL interface API
const { pathname } = new URL("https://example.net/aaa/bbb/ccc");
console.log(pathname) // "/aaa/bbb/ccc"
In the context of your code
fetch("/test/page/xxxx/", { redirect: "follow" }).then(res => {
if(res.status === 200 && res.redirected) {
const { pathname } = new URL(res.url);
this.props.push(pathname);
}
});
NOTE: Not supported in IE11 and below. If support for that browser is needed, there is a polyfill https://github.com/lifaon74/url-polyfill
There is also a proposal to be added to babel, expected soon to be a stage 0 feature

Redirecting from getInitalProps in React/Next.js

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')

Resources