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

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.

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.

Why even after fetching the data and logging it, whenever I render it to the page after reloading the application breaks in React?

I am undertaking one of the projects from frontendmentor.io
Whenever I click any of the mapped data, with the help of react-router-dom I am redirecting it to url/countryName.
In this scenario, I am using react-router-dom to fetch a particular country while using the useLocation hook to fetch the url, the problem is everything works fine in the first render but as soon as I reload the website the application breaks. No matter how many times I try it never renders again.
import axios from 'axios'
import React,{ useEffect,useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
const Country = () => {
const location = useLocation()
const name = location.pathname.split('/')[1]
const navigate = useNavigate()
const [country,setCountry] = useState({})
useEffect(() => {
const getCountry = async() => {
try {
const res = await axios.get(`https://restcountries.com/v3.1/name/${name.toLowerCase()}?fullText=true`)
console.log(res.data,res)
setCountry(res.data)
} catch (error) {
console.log(error)
}
}
getCountry()
}, [name])
console.log(country[0].name.common) // returns undefined after reload
const backButton = (event) => {
event.preventDefault()
navigate('/')
}
return (
<div className='country-page page'>
<button onClick={backButton} className='backButton'>back button</button>
<div className='country-page-layout'>
<h2></h2>
</div>
</div>
)
}
export default Country
Other resources:
error message screenshot
API which I am using : https://restcountries.com/
You need to wait for your data to be fetched first, and then render it.
const [country,setCountry] = useState({})
//country is an empty object
//but here, your are trying to get the value from the array. (But country is an empty object)
console.log(country[0].name.common) // returns undefined after reload
The solution is to check if your data is here first
if (country?.[0]?.name?.common){
console.log(country[0].name.common)
}

React Recoil: State not being saved across navigation to different URLs within App

I'm getting started with Recoil for a React App, but running into some issues, or at least some behavior I'm not expecting.
I'd like to be able to use one component to render many different "views" based on the URL. I have a useEffect in this component that switches based on the location.pathname and based on that pathname, it'll make an API call. But before it makes the API call, it checks the length of the atom to see if it's empty or not, then will call the API and set the atom based on the API call.
However, when I navigate to a different URL and come back to one I've already visited, the API is called again, even though I've previously set the state for that URL.
The behavior I'm expecting is that once a URL has been visited and the return from the API is stored in an Atom, the API call isn't made again when leaving the URL and coming back.
Relevant code below:
Atom.js
export const reports = atom({ key: "reports", default: { country: [], network: [], }, });
the one component that will render different data based on the reports atom.
import { useRecoilState } from "recoil";
import { reports } from "../globalState/atom";
const TableView = ({ columns, }) => {
const location = useLocation();
const [report, setReport] = useRecoilState(reports);
const currentView = location.pathname.split("/")[1];
useEffect(() => {
const getReportsData = async () => {
switch (location.pathname) {
case "/network":
if (report[currentView].length === 0) {
const response = await fetch("/api");
const body = await response.json();
setReport(
Object.assign({}, report, {
[currentView]: body,
})
);
console.log('ran')
break;
}
getReportsData();
}, [])
As previously mentioned, that console.log is ran every time I navigate to /network, even if I've already visited that URL.
I've also tried doing this with selectors.
Atom.js
export const networkState = atom({
key: "networkState",
default: networkSelector,
});
export const networkSelector = selector({
key: "networkSelector",
get: async ({ get }) => {
try {
const body = await fetch("/api/network").then((r) => r.json());
return body;
} catch (error) {
console.log(error);
return [];
}
}
Component
import {useRecoilStateLoadable} from "recoil"
import {networkState} from "../globalState/atom";
const Table = ({columns}) => {
const [networkData, setNetworkData] =
useRecoilStateLoadable(networkState);
And then a switch statement based on networkData.state
}
Any help would be greatly appreciated, thank you!

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)

Graphql query is firing every time the page is loading. How to save in cache?

I'm having a website where I show different kinds of projects to the user. I'm using Hasura, NextJS and Apollo to make this happen. The problem I have now is that every time I come back to the home page (where I show my projects) the query is getting fired again and has to load all over again. I want to save the data in my cache, but I'm not sure how to do that.
This is how I do it now:
I'm calling the query, when its done loading, I return my Home component with the data.
const ProjectList = () => {
const { loading, error, data } = useQuery(GET_PROJECTS);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
console.error(error);
return <div>Error!</div>;
}
return <Home projects={data.projects} />;
};
export default withApollo({ ssr: true })(ProjectList);
In the home component I know can use the data:
const Home = ({ projects }) => {
// rest code...
}
I create my apolloClient as the following:
export default function createApolloClient(initialState, headers) {
const ssrMode = typeof window === 'undefined';
let link;
if (ssrMode) {
link = createHttpLink(headers); // executed on server
} else {
link = createWSLink(); // executed on client
}
return new ApolloClient({
ssrMode,
link,
cache: new InMemoryCache().restore(initialState),
});
}
Is there a way I can save the data in the cache, so I don't have to wait for the loading state every time I come back to the home page?
You can use the prebuild feature of nextjs. For this you need to call getStaticProps in your ProjectList.jsx file. There you have to import your apollo client as well as your query, then initialize the client and do the query:
export async function getStaticProps() {
const apollo = require("../../../apollo/apolloClient"); // import client
const GET_PROJECTS = require("../../../apollo/graphql").GET_PROJECTS; // import query
const client = apollo.initializeApollo(); //initialize client
const { data, error } = await client.query({
query: GET_PROJECTS
});
if (!data || error) {
return {
notFound: true
};
}
return {
props: { projects: data.projects } // will be passed to the page component as props
};
}

Resources