Next JS nested routing - next.js

-component
---->sidebar.js
---->exampleTabOne.js
---->exampleTabTwo.js
---->exampleTabThree.js
--pages
---->setting(which include all sidebar and those exampletabs)
i do've above folder structure in my nextJS project.
here as per nextjs doc
on localhost/setting i can easily view my page
but what i want to achieve is something like below:
1.localhost/setting/exampleTabOne
2.localhost/setting/exampleTabTwo/EXAMPLEID
3.localhost/setting/exampleTabThree/EXAMPLEID#something#something
the last part Url with # is something like inside tab content i ve another tabs so i want to fix it with Hash url so that while ssr i can easily open that inside tab too..
So, will you guys please suggest me how to solve this?

Here , In Next JS, We can achieve this by defining in server.js file.
// This file doesn't go through babel or webpack transformation.
// Make sure the syntax and sources this file requires are compatible with the current node version you are running
// See https://github.com/zeit/next.js/issues/1245 for discussions on Universal Webpack or universal Babel
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
createServer((req, res) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true);
const { pathname, query } = parsedUrl;
if (pathname === '/setting/exampleTabOne') {
app.render(req, res, '/setting', query);
} else if (pathname === '/setting/exampleTabTwo/EXAMPLEID') {
app.render(req, res, '/setting', query);
} else {
handle(req, res, parsedUrl);
}
}).listen(3000, err => {
if (err) throw err;
console.log('> Ready on http://localhost:3000');
});
});
Where In setting page we can dynamically load the respective component watching url pathname.

Related

Vue3 how to use a dynamic path with defineAsyncComponnet

I am trying to add retry functionality to defineAsyncComponent so I created a helper function
const MAX_TRY = 3;
const WAIT_TIME = 1000;
async function loadAsyncComponent(componentPath: string, tryCount = 1) {
if (tryCount > MAX_TRY) return Promise.reject();
return new Promise((resolve, reject) => {
const path = componentPath.replace('.vue', '').replace('#/modules/', '');
// https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
import(`../../../modules/${path}.vue`).then(resolve).catch((error) => {
console.error('loading async component failed : ', error);
captureException(error);
wait(WAIT_TIME).then(() => {
loadAsyncComponent(componentPath, ++tryCount)
.then(resolve)
.catch(reject);
});
});
});
}
export default function customAsyncComponent<T>(componentPath: string): T {
return defineAsyncComponent({
// the loader function
loader: () => loadAsyncComponent(componentPath) as any,
// A component to use while the async component is loading
loadingComponent: Loading,
// Delay before showing the loading component. Default: 200ms.
delay: 200,
// A component to use if the load fails
errorComponent: AsyncLoadFailed,
// The error component will be displayed if a timeout is
// provided and exceeded. Default: Infinity.
timeout: 10000,
});
}
This works in local DEV environment but when I build and deployed to prod it broke Error: Unknown variable dynamic import: ../../../modules/room/components/room/Room.vue
It seems that these dynamic imports are not included in the final build.
Found this answer https://stackoverflow.com/a/72479802/1055015
Tryied these
rollupOptions: {
external: [path.resolve(__dirname, 'src/modules/room/components/room/Room.vue')],
},
rollupOptions: {
external: ['#/modules/room/components/room/Room.vue')],
},
After vite build The dynamically imported files are not generated in the dist folder
Any ideas ?
UPDATE: I found a workaround using glob-import testing it now
Solution 2:
The first solution left me with hundred's of small files , every single vue,ts,css file loading separately (~175),
I needed to preload them for better UX but and it started making 429 errors (too many request) on my server
So I made some changes on my custom loader and manually listed the files that needed to be preloaded
const PRELOAD_LIST = [
() => import('#/modules/X.vue'),
];
async function loadAsyncComponent(componentLoader: any, tryCount = 1) {
return new Promise((resolve, reject) => {
componentLoader() // <--------------- this part mostly
.then(resolve)
.catch((error) => {
console.error('loading async component failed : ', error);
captureException(error);
if (tryCount >= MAX_TRY) reject(error);
else {
wait(WAIT_TIME).then(() => {
loadAsyncComponent(componentLoader, ++tryCount)
.then(resolve)
.catch(reject);
});
}
});
});
}
const X = customAsyncComponent(() => import('#/modules/X.vue'));
Solution 1:
For now I used this approach :
const MODULES = import.meta.glob('../../../modules/**/*.vue');
this generates a key/value object list, the keys are the file path's and the values are import statements with static path
// code produced by vite
const MODULES = {
'../../../modules/dir/foo.vue': () => import('./modules/dir/foo.vue'),
'../../../modules/dir/bar.vue': () => import('./modules/dir/bar.vue')
}
Then you can just do MODULES[myPath]() to load the file/module.
This works but it also generates extra code for the modules at I don't really need to, so I am still looking for a better solution.

How do we get window.location.hash from a Next.js application?

If we stick window.location.hash in useEffect, it will always erroneously return '0'. Apparently it has to do with SSR.
I need to reliably be able to get the hash portion of the URL for my project. How should I best go about it?
Server side code needs to wait until the code is loaded in the browser to use browser APIs.
Vanilla js server-side compatible
const [isMounted, setMounted] = useState(false);
useEffect(() => {
if (isMounted) {
console.log('hash', window.location.hash);
} else {
setMounted(true);
}
}, [isMounted]);
if(!isMounted) return <>Loading...</>;
Using next/router
import { useRouter } from 'next/router';
const { asPath } = useRouter();
useEffect(()=>{
const hash = asPath.split('#')[1];
}, [ asPath ]);
FYI you're code shouldn't return a zero. The first culprit that comes to mind is when a shorthand condition is used without an else.
window && window.location.hash
this should have an else
(window && window.location.hash) || null
or
window && window.location.hash ? window.location.hash : null
Extending #Sean W's answer, if you want to get a specific hash value from a hash key, you can use URLSearchParams:
// example path: /#error=unauthorized_client&error_code=401error_description=Something+went+wrong
import { useRouter } from 'next/router';
const { asPath } = useRouter();
useEffect(() => {
const hash = (asPath as string).split("#")[1]; // error=unauthorized_client&error_code=401error_description=Something+went+wrong
const parsedHash = new URLSearchParams(hash);
const errorHash = parsedHash.get("error_description"); // Something went wrong
}, []); // `asPath` omitted from dependencies as ESLint states it won't change

excel4node fetch request with next.js api routes not triggering download

I am generating an excel file and want it to be downloaded for the client by triggering an API route using the Next.js framework. I am having trouble triggering the download by using fetch. The download can be triggered by window.open(//urlhere, '_self') but the API call using fetch gives this response on request:
API resolved without sending a response for /api/download?Students= this may result in stalled requests.
The excel4node documentation says we can send an excel document through an API like this:
// sends Excel file to web client requesting the / route
// server will respond with 500 error if excel workbook cannot be generated
var express = require('express');
var app = express();
app.get('/', function(req, res) {
wb.write('ExcelFile.xlsx', res);
});
app.listen(3000, function() {
console.log('Example app listening on port 3000!');
});
Here is my backend download.js which lives in pages/api/:
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import Link from "next/link";
import getPartners from "../../components/main";
import excel from "excel4node";
export default function handler(req, res) {
const students = req.query.Students.split(",");
const partners = JSON.stringify(getPartners(students));
let workbook = createExcelList(partners);
workbook.write("PartnerList.xlsx", res);
}
const createExcelList = (partnersJSON) => {
const workbook = new excel.Workbook();
const partnersObject = JSON.parse(partnersJSON);
/* Using excel4node a workbook is generated formatted the way I want */
return workbook;
};
export const config = {
api: {
bodyParser: true,
},
};
And here is the function that is triggered on a button press in the front end.
const makePartners = async () => {
let queryStudents = studentList.join(",");
const url = "http://localhost:3000/api/download?Students=" + queryStudents;
if (studentList.length !== 0) {
try {
const res = await fetch(url, {
headers: {
"Content-Type":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
});
console.log(res);
} catch (e) {
console.log(e);
}
}
};
Which does not trigger the download. But using window.open(url, '_self) does. So, I can trigger the download by changing the function to the following. However I don't think this is the correct way of doing things and would like to be able to understand how to use fetch correctly.
const makePartners = () => {
let queryStudents = studentList.join(",");
const url = "http://localhost:3000/api/download?Students=" + queryStudents;
if (studentList.length !== 0) {
window.open(url, "_Self");
}
};
I am not sure if this is a Next.js issue or not. Does anyone have any insight? Any help would be appreciated.

How can you handle trailing slashes in next.js routes?

I am trying to set up a next.js app, but I'm having trouble handling routes with a trailing slash. So, for example, if I have a pages structure like this:
pages
- index.js
- blog
- index.js
- [slug].js
then going to / gives me the base index.js, going to /blog gives me blog/index.js, and going to /blog/my-post gives me blog/[slug].js — so far so good.
But going to /blog/ gives me a 404 error, and there appears to be no way at all to handle this without entirely replacing the next.js router—I can't even redirect /blog/ to /blog. Is there any way around this, or do I need a custom router? Is there a way to extend the next.js router in a way that will let me handle these, without entirely replacing it?
There is an option with Next.js 9.5 and up.
In next.config.js, add the trailingSlash config:
module.exports = {
trailingSlash: true,
}
Source: Trailing Slash
UPDATE: If you are using next export than you can solve the issue by adding exportTrailingSlash to your next.config.js
As of this writing, there seems to be no way to solve this issue without defining your own custom server.
Previous answer:
You must create a new file blog.js shown bellow:
With the following server.js
const express = require('express')
const next = require('next')
const PORT = process.env.PORT || 3000;
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app
.prepare()
.then(() => {
const server = express()
server.get('/blog', (req, res) => {
const actualPage = '/blog'
// const queryParams = { title: req.params.id }
app.render(req, res, '/blog', {})
})
server.get('/blog/:id', (req, res) => {
const actualPage = '/blog/[id]'
const queryParams = { title: req.params.id }
app.render(req, res, actualPage, queryParams)
})
server.get('*', (req, res) => {
return handle(req, res)
})
server.listen(PORT, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${PORT}`)
})
})
.catch(ex => {
console.error(ex.stack)
process.exit(1)
})
node server.js should start your server and you would have the mapping that you need.
Note, blog/index.js is not used in this example.
I don't have a solution to this. This is still a massive problem. However, I do have a very horrible hack that I've been using to workaround this problem in Next.js 9.x.x
In my /pages/_error.js I've added the following code:
FourOhFour.getInitialProps = ({asPath, res}: any): any => {
if (asPath.endsWith('/')) {
res.writeHead(301, {Location: asPath.substring(0, asPath.length - 1)});
return res.end();
}
return {};
};
This will ensure that all routes that end with a trailing slash are redirected to the non-trailing slash path via a 301 redirect.
I`ve done that redirect by using nginx for my production app:
rewrite ^(.+)/+$ $1 permanent;
You can add this to your _app.js file
MyApp.getInitialProps = async ctx => {
const pathAndQueryDivided = ctx.ctx.req.url.split('?');
if (pathAndQueryDivided[0].endsWith('/')) {
const urlWithoutEndingSlash = pathAndQueryDivided[0].replace(/\/*$/gim, '');
ctx.ctx.res.writeHead(301, {
Location: urlWithoutEndingSlash + (pathAndQueryDivided.length > 1 ? `?${pathAndQueryDivided[1]}` : ''),
});
ctx.ctx.res.end();
return {};
}
const initialProps = await App.getInitialProps(ctx);
return {
...initialProps,
};
};
App.getInitialProps gets called first server-side
It divides splits the path and the query params because we only want to know if the url ends with a slash
It asks if the url ends with a slash
It replaces the last slash of the url with nothing
It redirects you to the url without the slash + the query params if there's any

Next.JS deprecation message with custom server

I'm using a custom server.js for routing which uses something like this:
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
const rootStaticFiles = ['/robots.txt', '/sitemap.xml', '/favicon.ico'];
if (rootStaticFiles.indexOf(parsedUrl.pathname) > -1) {
const path = join(__dirname, 'static', parsedUrl.pathname);
app.serveStatic(req, res, path);
} else {
handler(req, res);
}
}).listen(3000);
});
And when I run it, there's this warning about the 'url' property, but I don't know ho to resolve it. It seems to me that withRouter doesn't apply here. Could someone help please?
Okay, it looks like they solved it with one of the updates. It does not appear anymore.

Resources