NextJs and dynamic routes - how to handle the empty parameter scenario? - reactjs

I'm trying to retrieve the url parameter without using query strings, for example, http://localhost:3000/test/1, this is what I have so far:
Dir structure
test
- [pageNumber].jsx
index.jsx
import React from 'react';
import { useRouter } from 'next/router';
const Index = () => {
const router = useRouter();
return (
<>
<h1>Page number: {router.query.pageNumber}</h1>
</>
);
};
export default Index;
It works, but if I omit the pageNumber param, all I got is a 404 page, an issue we don't have on using query strings.
Now, the question: is it possible to sort this without creating an additional index.jsx page and duplicating code to handle the empty parameter scenario?

I see I may as well answer this as I got notified of MikeMajaras comment.
There are several ways of doing this, say you have a route that gets posts from a user and shows 10 posts per pages so there is pagination.
You could use an optional catch all route by creating pages/posts/[[...slug]].js and get the route parameters in the following way:
const [user=DEFAULT_USER,page=DEFAULT_PAGE] = context?.query?.slug || [];
"Disadvantage" is that pages/posts/user/1/nonexisting doesn't return a 404.
You could create pages/posts/index.js that implements all code and pages/posts/[user]/index.js and pages/posts/[user]/[page]/index.js that just export from pages/posts/index.js
export { default, getServerSideProps } from '../index';
You get the query parameters with:
const {user=DEFAULT_USER,page=DEFAULT_PAGE} = context.query;
You could also just only create pages/posts/[user]/[page]/index.js and implement all code in that one and config a redirect
module.exports = {
async redirects() {
return [
{
source: '/posts',
destination: `/posts/DEFAULT_USER`,
permanent: true,
},
{
source: '/posts/:user',
destination: `/posts/:user/DEFAULT_PAGE`,
permanent: true,
},
];
},
};

Related

Why does react-query query not work when parameter from router.query returns undefined on refresh?

When page is refreshed query is lost, disappears from react-query-devtools.
Before Next.js, I was using a react and react-router where I would pull a parameter from the router like this:
const { id } = useParams();
It worked then. With the help of the, Next.js Routing documentation
I have replaced useParams with:
import { usePZDetailData } from "../../hooks/usePZData";
import { useRouter } from "next/router";
const PZDetail = () => {
const router = useRouter();
const { id } = router.query;
const { } = usePZDetailData(id);
return <></>;
};
export default PZDetail;
Does not work on refresh. I found a similar topic, but manually using 'refetch' from react-query in useEffects doesn't seem like a good solution. How to do it then?
Edit
Referring to the comment, I am enclosing the rest of the code, the react-query hook. Together with the one already placed above, it forms a whole.
const fetchPZDetailData = (id) => {
return axiosInstance.get(`documents/pzs/${id}`);
};
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {});
};
Edit 2
I attach PZList page code with <Link> implementation
import Link from "next/link";
import React from "react";
import TableModel from "../../components/TableModel";
import { usePZSData } from "../../hooks/usePZData";
import { createColumnHelper } from "#tanstack/react-table";
type PZProps = {
id: number;
title: string;
entry_into_storage_date: string;
};
const index = () => {
const { data: PZS, isLoading } = usePZSData();
const columnHelper = createColumnHelper<PZProps>();
const columns = [
columnHelper.accessor("title", {
cell: (info) => (
<span>
<Link
href={`/pzs/${info.row.original.id}`}
>{`Dokument ${info.row.original.id}`}</Link>
</span>
),
header: "Tytuł",
}),
columnHelper.accessor("entry_into_storage_date", {
header: "Data wprowadzenia na stan ",
}),
];
return (
<div>
{isLoading ? (
"loading "
) : (
<TableModel data={PZS?.data} columns={columns} />
)}
</div>
);
};
export default index;
What you're experiencing is due to the Next.js' Automatic Static Optimization.
If getServerSideProps or getInitialProps is present in a page, Next.js
will switch to render the page on-demand, per-request (meaning
Server-Side Rendering).
If the above is not the case, Next.js will statically optimize your
page automatically by prerendering the page to static HTML.
During prerendering, the router's query object will be empty since we
do not have query information to provide during this phase. After
hydration, Next.js will trigger an update to your application to
provide the route parameters in the query object.
Since your page doesn't have getServerSideProps or getInitialProps, Next.js statically optimizes it automatically by prerendering it to static HTML. During this process the query string is an empty object, meaning in the first render router.query.id will be undefined. The query string value is only updated after hydration, triggering another render.
In your case, you can work around this by disabling the query if id is undefined. You can do so by passing the enabled option to the useQuery call.
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {
enabled: id
});
};
This will prevent making the request to the API if id is not defined during first render, and will make the request once its value is known after hydration.

How to render [slug].js page after fetching data Next.js

I am trying to create a logic for my blog/:post page in Next.js but I cannot seem to figure out how.
The idea is to:
Fetch the url (using useRouter)
Call API (it is a headless CMS) to get the info of the post
Render the post
What I have right now is:
[other imports ...]
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
function BlogPost(props) {
const router = useRouter()
const { slug } = router.query
const [blogPost, setBlogPost] = useState({})
// Function to the blog post
function fetchBlogPost() {
butter.post.retrieve(slug)
.then(response => {
const blogPostData = response.data.data
setBlogPost(blogPostData)
})
}
useEffect(() => {
// We need to add this if condition because the router wont grab the query in the first render
if(!router.isReady) return;
fetchBlogPost()
}, [router.isReady])
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
But this is not rendering everything (the image is not being rendered for example). I believe it is because of the pre-render functionality that Next.js has. Also I have been reading about the getStaticProps and getStaticPaths but I am unsure on how to use them properly.
Any guidance will be welcome. Thanks!
If you're using next.js then you are on track with getStaticProps being your friend here!
Essentially getStaticProps allows you to take advantage of ISR to fetch data on the server and create a static file of your page with all of the content returned from the fetch.
To do this you'll need to make an adjustment to your current architecture which will mean that instead of the slug coming in from a query param it will be a path parameter like this: /blogs/:slug
Also this file will need to be called [slug].js and live in (most likely) a blogs directory in your pages folder.
Then the file will look something like this:
import { useEffect, useRef, useState } from "react";
import { useRouter } from 'next/router'
const apikey = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY;
const butter = require('buttercms')(apikey);
export const getStaticPaths = async () => {
try {
// You can query for all blog posts here to build out the cached files during application build
return {
paths:[], // this would be all of the paths returned from your query above
fallback: true, // allows the component to render with a fallback (loading) state while the app creates a static file if there isn't one available.
}
} catch (err) {
return {
paths: [],
fallback: false,
}
}
}
export const getStaticProps = async ctx => {
try {
const { slug } = ctx.params || {}
const response = await butter.post.retrieve(slug)
if(!response.data?.data) throw new Error('No post data found') // This will cause a 404 for this slug
return {
notFound: false,
props: {
postData: response.data.data,
slug,
},
revalidate: 5, // determines how long till the cached static file is invalidated.
}
} catch (err) {
return {
notFound: true,
revalidate: 5,
}
}
}
function BlogPost(props) {
const {isFallback} = useRouter() // We can render a loading state while the server creates a new page (or returns a 404).
const {postData} = props
// NOTE: postData might be undefined if isFallback is true
return (
<>
# Render post with the data fetched
</>
)
}
export default BlogPost;
In any case, though if you decide to continue with rendering on the client instead then you might want to consider moving your fetch logic inside of the useEffect.

How to get URL query string on Next.js static site generation?

I want to get query string from URL on Next.js static site generation.
I found a solution on SSR but I need one for SSG.
Thanks
import { useRouter } from "next/router";
import { useEffect } from "react";
const router = useRouter();
useEffect(() => {
if(!router.isReady) return;
const query = router.query;
}, [router.isReady, router.query]);
It works.
I actually found a way of doing this
const router = useRouter()
useEffect(() => {
const params = router.query
console.log(params)
}, [router.query])
As other answers mentioned, since SSG doesn't happen at request time, you wouldn't have access to the query string or cookies in the context, but there's a solution I wrote a short article about it here https://dev.to/teleaziz/using-query-params-and-cookies-in-nextjs-static-pages-kbb
TLDR;
Use a middleware that encodes the query string as part of the path,
// middleware.js file
import { NextResponse } from 'next/server'
import { encodeOptions } from '../utils';
export default function middleware(request) {
if (request.nextUrl.pathname === '/my-page') {
const searchParams = request.nextUrl.searchParams
const path = encodeOptions({
// you can pass values from cookies, headers, geo location, and query string
returnVisitor: Boolean(request.cookies.get('visitor')),
country: request.geo?.country,
page: searchParams.get('page'),
})
return NextResponse.rewrite(new URL(`/my-page/${path}`, request.nextUrl))
}
return NextResponse.next()
}
Then make your static page a folder that accepts a [path]
// /pages/my-page/[path].jsx file
import { decodeOptions } from '../../utils'
export async function getStaticProps({
params,
}) {
const options = decodeOptions(params.path)
return {
props: {
options,
}
}
}
export function getStaticPaths() {
return {
paths: [],
fallback: true
}
}
export default function MyPath({ options }) {
return <MyPage
isReturnVisitor={options.returnVisitor}
country={options.country} />
}
And your encoding/decoding functions can be a simple JSON.strinfigy
// utils.js
// https://github.com/epoberezkin/fast-json-stable-stringify
import stringify from 'fast-json-stable-stringify'
export function encodeOptions(options) {
const json = stringify(options)
return encodeURI(json);
}
export function decodeOptions(path) {
return JSON.parse(decodeURI(path));
}
You don't have access to query params in getStaticProps since that's only run at build-time on the server.
However, you can use router.query in your page component to retrieve query params passed in the URL on the client-side.
// pages/shop.js
import { useRouter } from 'next/router'
const ShopPage = () => {
const router = useRouter()
console.log(router.query) // returns query params object
return (
<div>Shop Page</div>
)
}
export default ShopPage
If a page does not have data fetching methods, router.query will be an empty object on the page's first load, when the page gets pre-generated on the server.
From the next/router documentation:
query: Object - The query string parsed to an object. It will be
an empty object during prerendering if the page doesn't have data
fetching
requirements.
Defaults to {}
As #zg10 mentioned in his answer, you can solve this by using the router.isReady property in a useEffect's dependencies array.
From the next/router object documentation:
isReady: boolean - Whether the router fields are updated
client-side and ready for use. Should only be used inside of
useEffect methods and not for conditionally rendering on the server.
you don't have access to the query string (?a=b) for SSG (which is static content - always the same - executed only on build time).
But if you have to use query string variables then you can:
still statically pre-render content on build time (SSG) or on the fly (ISR) and handle this route by rewrite (next.config.js or middleware)
use SSR
use CSR (can also use SWR)

How to access dynamic route parameters when preloading server-side render

I am attempting to render a dynamic route preloaded with data fetched via an async thunk.
I have a static initialAction method in my Components that require preloading, and let them call the actions as needed. Once all actions are done and promises are resolved, I render the route/page.
The question that I have is: how do I reference the route parameters and/or props inside a static function?
Here is the relevant code that will call any initialAction functions that may be required to preload data.
const promises = routes.reduce((promise, route) => {
if (matchPath(req.url, route) && route.component && route.component.initialAction) {
promise.push(Promise.resolve(store.dispatch(route.component.initialAction(store))))
}
return promise;
}, []);
Promise.all(promises)
.then(() => {
// Do stuff, render, etc
})
In my component, I have the static initialAction function that will take in the store (from server), and props (from client). One way or the other, the category should be fetched via redux/thunk. As you can see, I'm not passing the dynamic permalink prop when loading via the server because I'm unable to retrieve it.
class Category extends Component {
static initialAction(store, props) {
let res = store !== null ? getCategory(REACT_APP_SITE_KEY) : props.getCategory(REACT_APP_SITE_KEY, props.match.params.permalink)
return res
}
componentDidMount(){
if(isEmpty(this.props.category.categories)){
Category.initialAction(null, this.props)
}
}
/* ... render, etc */
}
And finally, here are the routes I am using:
import Home from '../components/home/Home'
import Category from '../components/Category'
import Product from '../components/product/Product'
import NotFound from '../components/NotFound'
export default [
{
path: "/",
exact: true,
component: Home
},
{
path: "/category/:permalink",
component: Category
},
{
path: "/:category/product/:permalink",
component: Product
},
{
component: NotFound
}
]
Not entirely sure I'm even doing this in a "standard" way, but this process works thus far when on a non-dynamic route. However, I have a feeling I'm waaaay off base :)
on the server you have request object and on the client, you have location object. Extract url parameters from these objects after checking current environment.
const promises = routes.reduce((promise, route) => {
var props = matchPath(req.url, route);
if ( obj && route.component && route.component.initialAction) {
promise.push(Promise.resolve(store.dispatch(route.component.initialAction(store, props))))
}
return promise;
}, []);
now you will get url in initialAction in both desktop and server. you can extract dynamic route params from this

How to preload data with react-router v4?

This is example from official docs (https://reacttraining.com/react-router/web/guides/server-rendering/data-loading):
import { matchPath } from 'react-router-dom'
// inside a request
const promises = []
// use `some` to imitate `<Switch>` behavior of selecting only
// the first to match
routes.some(route => {
// use `matchPath` here
const match = matchPath(req.url, route)
if (match)
promises.push(route.loadData(match))
return match
})
Promise.all(promises).then(data => {
// do something w/ the data so the client
// can access it then render the app
})
This documentation makes me very nervous. This code doesn't work. And this aproach doesn't work! How can I preload data in server?
This Is what I have done - which is something I came up with from the docs.
routes.cfg.js
First setup the routes config in a way that can be used for the client-side app and exported to used on the server too.
export const getRoutesConfig = () => [
{
name: 'homepage',
exact: true,
path: '/',
component: Dasboard
},
{
name: 'game',
path: '/game/',
component: Game
}
];
...
// loop through config to create <Routes>
Server
Setup the server routes to consume the config above and inspect components that have a property called needs (call this what you like, maybe ssrData or whatever).
// function to setup getting data based on routes + url being hit
async function getRouteData(routesArray, url, dispatch) {
const needs = [];
routesArray
.filter((route) => route.component.needs)
.forEach((route) => {
const match = matchPath(url, { path: route.path, exact: true, strict: false });
if (match) {
route.component.needs.forEach((need) => {
const result = need(match.params);
needs.push(dispatch(result));
});
}
});
return Promise.all(needs);
}
....
// call above function from within server using req / ctx object
const store = configureStore();
const routesArray = getRoutesConfig();
await getRouteData(routesArray, ctx.request.url, store.dispatch);
const initialState = store.getState();
container.js/component.jsx
Setup the data fetching for the component. Ensure you add the needs array as a property.
import { connect } from 'react-redux';
import Dashboard from '../../components/Dashboard/Dashboard';
import { fetchCreditReport } from '../../actions';
function mapStateToProps(state) {
return { ...state.creditReport };
}
const WrappedComponent = connect(
mapStateToProps,
{ fetchCreditReport }
)(Dashboard);
WrappedComponent.needs = [fetchCreditReport];
export default WrappedComponent;
Just a note, this method works for components hooked into a matching routes, not nested components. But for me this has always been fine. The component at route level does the data fetch, then the components that need it later either has it passed to them or you add a connector to get the data direct from the store.

Resources