3 levels nested dynamic routes with Next.js - next.js

I'm trying to achieve a 3 levels dynamic routes in my next js application. So far I just obtain a 2 levels dynamic routes. All the data are coming from a headless CMS. I'm using graphlQL
So far I managed to obtain a 2 levels: blog/[category]/[post]. However I can't make this working: blog/[category]/[subcategory]/[post]
This is the code for the category:
import Link from "next/link";
import { getCategories, getPosts } from "../../../utils/datocmsHelper";
export default function CategoriesPage({ category, subcategories, posts }) {
return (
<div>
<h1>{category.categoryName}</h1>
<h2>sub-categories inside Category:</h2>
<ul>
{subcategories.map((subcategory) => (
<li key={subcategory.slug}>
<Link href={`/blog/${category.slug}/${subcategory.slug}`}>
{subcategory.slug}
</Link>
</li>
))}
</ul>
<h2>Posts:</h2>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${category.slug}/${post.slug}`}>
{post.slug}
</Link>
</li>
))}
</ul>
</div>
);
}
export async function getStaticProps(ctx) {
const categoriesData = await getCategories();
const postsData = await getPosts();
const currentCategory = categoriesData.allCategories.find(
(category) => category.slug === ctx.params.category
);
const subcategoriesInCategory = categoriesData.allCategories.filter(
(subcategory) => subcategory.category.slug === currentCategory.slug
);
const postsInCategory = postsData.allPosts.filter(
(post) => post.category.slug === currentCategory.slug
);
return {
props: {
category: currentCategory,
subcategories: subcategoriesInCategory,
posts: postsInCategory,
},
};
}
export async function getStaticPaths() {
const categoriesData = await getCategories();
return {
paths: categoriesData.allCategories.map((category) => ({
params: {
category: category.slug,
},
})),
fallback: false,
};
}
this is the code for the subcategory:
import { getPosts, getCategories } from "../../../../utils/datocmsHelper";
export default function CategoriesPage({ subcategory }) {
return (
<div className="flex flex-col">
<h1>title: {subcategory.slug}</h1>
<p>Category: {subcategory.category.slug}</p>
<p>subcategory general content</p>
</div>
);
}
export async function getStaticProps(ctx) {
const subcatogoriesData = await getCategories();
const currentsubcategory = subcatogoriesData.allCategories.find(
(subcategory) => subcategory.slug === ctx.params.subcategory
);
return {
props: {
subcategory: currentsubcategory,
},
};
}
export async function getStaticPaths() {
const catogoriesData = await getCategories();
return {
paths: catogoriesData.allCategories.map((category) =>
category.children.map((subcategory, i) => ({
params: {
subcategory: subcategory.slug,
category: category.category.slug,
},
}))),
fallback: false,
};
}
and this for the post:
import { getPosts } from "../../../../../utils/datocmsHelper";
export default function CategoriesPage({ post }) {
return (
<div className="flex flex-col">
<h1>title: {post.title}</h1>
<p>Category: {post.category.slug}</p>
<p>post general content</p>
</div>
);
}
export async function getStaticProps(ctx) {
const postsData = await getPosts();
const currentPost = postsData.allPosts.find(
(post) => post.slug === ctx.params.post
);
return {
props: {
post: currentPost,
},
};
}
export async function getStaticPaths() {
const postsData = await getPosts();
return {
paths: postsData.allPosts.map((post) => ({
params: {
post: post.slug,
category: post.category.slug,
},
})),
fallback: false,
};
}

One way is to just use an index file at the end.
blog/[category]/[subcategory]/[post]/index.tsx
// can access category/subcategory/post values e.g. at blog/general/travel/5/
Next.js dynamic routes

Related

useContext is not showing updated state when user navigates to the next page although cookie has updated data

I have an AddToCart component that calls AddToCart action on context.
context.ts:
import React, { useContext, useReducer, useEffect } from "react";
import reducer from "./reducer";
import Cookies from "js-cookie";
const initialCart = {
cart: Cookies.get("cart") ? JSON.parse(Cookies.get("cart")!) : [],
isShowing: false,
loading: false,
lastAdded: null,
total: 0,
amount: 0,
};
const CartContext = React.createContext<any>(initialCart);
const CartProvider = ({ children }: { children: any }) => {
const [state, dispatch] = useReducer(reducer, initialCart);
const showModal = () => {
dispatch({ type: "SHOW_MODAL" });
};
const hideModal = () => {
dispatch({ type: "HIDE_MODAL" });
};
const addToCart = (product: any) => {
dispatch({ type: "ADD_TO_CART", payload: product });
};
return (
<CartContext.Provider
value={{
...state,
showModal,
hideModal,
addToCart,
}}
>
{children}
</CartContext.Provider>
);
};
// make cart context available to child components
export const useCartContext = () => {
return useContext(CartContext);
};
export { CartContext, CartProvider };
reducer.ts: (excuse my terrible use of types, will fix)
import Cookies from "js-cookie";
const reducer = (state: any, action: any) => {
if (action.type === "SHOW_MODAL") {
return { ...state, isShowing: true };
}
if (action.type === "HIDE_MODAL") {
return { ...state, isShowing: false };
}
if (action.type === "ADD_TO_CART") {
const {
Color,
MPN,
SKU,
SO,
Size,
UPC,
manufacturerID,
price,
productID,
productName,
qtyInStock,
pic,
productURI,
} = action.payload;
const tempItem = state.cart.find((item: any) => {
return item.productID === productID;
});
// if item being added to cart already exists in cart:
if (tempItem) {
let newCart = state.cart.map((cartItem: any) => {
if (cartItem.productID === productID) {
let newQty = cartItem.qty + 1;
if (newQty > cartItem.qtyInStock) {
newQty = cartItem.qtyInStock;
// replace this with toast for a more professional looking alert
window.alert("No additional items available");
return { ...cartItem };
}
state.isShowing = true;
state.lastAdded = cartItem;
// bring user to top of page
window.scrollTo({
top: 0,
behavior: "smooth",
});
return { ...cartItem, qty: newQty };
} else {
return { ...cartItem };
}
});
return {
...state,
cart: [...newCart],
isShowing: state.isShowing,
lastAdded: state.lastAdded,
};
} else {
let newItem = {
Color,
MPN,
SKU,
SO,
Size,
UPC,
manufacturerID,
price,
productID,
productName,
qtyInStock,
pic,
qty: 1,
productURI,
};
// bring user to top of page
window.scrollTo({
top: 0,
behavior: "smooth",
});
Cookies.set("cart", JSON.stringify([...state.cart, newItem]));
const prevState = { ...state };
// return updated state
return {
...prevState,
cart: [...state.cart, newItem],
isShowing: true,
lastAdded: { ...newItem },
};
}
}
throw new Error("no matching action type");
};
export default reducer;
cart.tsx:
import React from "react";
import Layout from "../components/layout/Layout";
import { useContext } from "react";
import { CartContext } from "../modules/cart/context";
import CouponCode from "../components/cart/CouponCode";
import dynamic from "next/dynamic";
import CartItem from "../components/cart/CartItem";
const Cart = () => {
const context = useContext(CartContext);
return (
<Layout>
<div
className={`sm:container flex mx-auto`}
style={{ maxWidth: "1100px" }}
>
<div className={`flex w-full mx-auto`}>
<div className={`basis-2/3 mr-8`}>
<h3 className={`text-2xl font-leagueSpartan mb-4`}>Bag</h3>
<div className={``}>
{context.cart.map((product: any, index: number) => {
return <CartItem key={index} product={product} />;
})}
</div>
</div>
<div>
<h3 className={`text-2xl font-leagueSpartan mb-4`}>Summary</h3>
<div>Do you have a coupon code?</div>
<CouponCode />
<div>
<div>Subtotal:</div>
<div>Estimated Shipping & Handling:</div>
<div>Estimated Tax:</div>
</div>
<hr className={`my-8 h-px bg-gray-400 border-0`}></hr>
<div>Total: </div>
<hr className={`my-8 h-px bg-gray-400 border-0`}></hr>
</div>
</div>
</div>
</Layout>
);
};
export default dynamic(() => Promise.resolve(Cart), { ssr: false });
After adding to cart and navigating to the cart page, no cart item is shown (the item is stored in the cart cookie correctly). When the page is refreshed, the item is shown.
I initially thought this has to do with deep cloning and that react is not acknowledging that a change is made to the cart array. Upon further research, I think I am using the spread operator correctly, but my next step will be to use lodash deep clone just in case. Other than that, I'm just reviewing the code for a design patter flaw that would prevent state from syncing with the cookie. Thanks!

Component is being rendered twice in SSG page in Next.js

I'm currently trying using utterances, which is a github-based open source for comments.
I'm using utterances in my SSG page. Therefore, I'm using client side rendering for getting the utterances component.
Here is the code.
// blog/[id].tsx
/* eslint-disable react/no-danger */
import axios from 'axios';
import { dateFormat } from '_Utils/Helper';
import MarkdownRenderer from '_Components/MarkdownRenderer';
import Comment from '_Components/Comment';
import styles from './blog.module.scss';
const Article = ({ article }: any) => {
return (
<div className={styles.container}>
<div className={styles.header}>
<p className={styles.tag}>{article.data.attributes.tag.data.attributes.tag}</p>
<h1>{article.data.attributes.title}</h1>
<p className={styles.publishedDate}>Published at {dateFormat(article.data.attributes.publishedAt)}</p>
</div>
<main
>
<MarkdownRenderer markdown={article.data.attributes.content} />
<Comment />
</main>
</div>
);
};
export async function getStaticPaths() {
const articlePaths: any = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/api/articles/?populate[0]=*`);
const paths = articlePaths.data.data.map((path: any) => ({
params: { id: `${path.id}` },
}));
return { paths, fallback: false };
}
export async function getStaticProps(ctx: any) {
const { params } = ctx;
const { id } = params;
const article = await axios.get(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/articles/${id}?populate[1]=tag&populate[0]=thumbnail`
);
return {
props: { article: article.data },
};
}
export default Article;
// Comment
const Comment = () => {
return (
<section
style={{ height: '350px', width: '100%' }}
ref={(elem) => {
if (!elem) {
return;
}
const scriptElem = document.createElement('script');
scriptElem.src = 'https://utteranc.es/client.js';
scriptElem.async = true;
scriptElem.setAttribute('repo', 'usernamechiho/Cobb-dev-blog');
scriptElem.setAttribute('issue-term', 'title');
scriptElem.setAttribute('theme', 'github-light');
scriptElem.setAttribute('label', 'comment');
scriptElem.crossOrigin = 'anonymous';
elem.appendChild(scriptElem);
}}
/>
);
};
export default Comment;
and the result
I was wondering why it happens and tried dynamic import with ssr: false.
However, there was nothing but the same.
Is there anything I can look for to get through this?

How to update page when data is changed in nextjs?

When I delete one of the notes, it deletes from the DB. And to see the effect, I need to reload the page every time I delete a note.
How do I see the not deleted notes without reloading the page?
Here's the code for my page:
export default function Home(notes) {
const [notesData, setNotesData] = useState(notes);
const deleteNote = async (note) => {
const res = await fetch(`http://localhost:3000/api/${note}`, {
method: "DELETE",
});
};
return (
<div>
<h1>Notes:</h1>
{notesData.notes.map((note) => {
return (
<div className="flex">
<p>{note.title}</p>
<p onClick={() => deleteNote(note.title)}>Delete</p>
</div>
);
})}
</div>
);
}
export async function getServerSideProps() {
const res = await fetch(`http://localhost:3000/api`);
const { data } = await res.json();
return { props: { notes: data } };
}
If you're fetching the data with getServerSideProps you need to recall that in order to get the updated data like this :
import { useRouter } from 'next/router';
const router = useRouter()
const refreshData = () => router.replace(router.asPath);
But also you can store the data from getServerSideProps in a state and render that state and trigger a state update after a note is deleted like this :
export default function Home(notes) {
const [notesData, setNotesData] = useState(notes);
const deleteNote = async (note) => {
const res = await fetch(`http://localhost:3000/api/${note}`, {
method: "DELETE",
});
};
return (
<div>
<h1>Notes:</h1>
{notesData.notes.map((note) => {
return (
<div className="flex">
<p>{note.title}</p>
<p onClick={() => deleteNote(note.title).then(()=>{
const res = await fetch(`http://localhost:3000/api`);
const { data } = await res.json();
setNotesData(data)
})
}>Delete</p>
</div>
);
})}
</div>
);
}
export async function getServerSideProps() {
const res = await fetch(`http://localhost:3000/api`);
const { data } = await res.json();
return { props: { notes: data } };
}

infinite scroll with get static props and grapql not working

I'm working on a project with pokeapi graphql and I made a infinite scroll component that loads more pokemon when you scroll the page. I wanted to have the first 48 pokemons pre loaded with static generation.
So I have my index page the following code:
const Home = ({ fallback }): JSX.Element => {
return (
<div>
<SWRConfig value={fallback}>
<PokemonList />
</SWRConfig>
</div>
);
};
export const getStaticProps: GetStaticProps = async () => {
const url =
'species: pokemon_v2_pokemonspecies(order_by: {id: asc}, limit: 24, offset: 0)';
const pokemonList = await getPokemonListData({
url,
});
return {
props: {
fallback: {
'species: pokemon_v2_pokemonspecies(order_by: {id: asc}, limit: 24, offset: 0)':
pokemonList,
},
},
revalidate: 60 * 60 * 24, // 24 hours
};
};
And I use this custom hook for the data:
import getPokemonListData from '#requests/getPokemonListData';
import useSWRInfinite from 'swr/infinite';
interface IUsePokemonListParams {
limit?: number;
}
interface IUsePokemonListReponse {
pokemonList: IBasicPokemonInfo[][];
isLoading: boolean;
size: number;
setSize: (
size: number | ((_size: number) => number),
) => Promise<IBasicPokemonInfo[][]>;
}
export default function usePokemonList({
limit,
}: IUsePokemonListParams): IUsePokemonListReponse {
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null; // reached the end
return `species: pokemon_v2_pokemonspecies(order_by: {id: asc}, limit: ${limit}, offset: ${
pageIndex * limit
})`;
};
const { data, error, size, setSize } = useSWRInfinite(getKey, url =>
getPokemonListData({ url }),
);
return {
pokemonList: data,
isLoading: !error && !data,
size,
setSize,
};
}
on my list component I use the custom hook and list the data in another component:
const PokemonList = (): JSX.Element => {
const loader = useRef(null);
const { pokemonList, setSize } = usePokemonList({ limit: 24 });
useController({ loader, setSize }); // this is my infinite scroll logic, I set the size when I reach the page limit
useEffect(() => {
document.body.className = 'initial';
}, []);
return (
<>
<ol className={styles.cardContainer}>
<>
{pokemonList.map((list, index) => (
<Fragment key={index}>
{list.map(pokemon => (
<li key={pokemon.id}>
<Pokemon
id={pokemon.id}
name={pokemon.name}
types={pokemon.types}
image={pokemon.image}
/>
</li>
))}
</Fragment>
))}
</>
</ol>
<div ref={loader} />
</>
);
};
However, when I call the custom hook on my list component, for some reason the data returned from the hook, in this case the "pokemonList", is undefined and the request has to be made again. Is there something that I'm missing?
You have a mistake in your SWRConfig. Instead of <SWRConfig value={fallback}> you should have <SWRConfig value={{fallback}}>

Redux actions without return or dispatch

I am implementing Oauth from google with redux, and I wanted to have all google API calls handled from my redux and ended up writing helper functions in my actions file that doesn't return anything or call dispatch. I ended up with code where I only dispatch once from my JSX file and wondering if this is okay or there is another better way to do it?
The code is as follows:
authActions.js
const clientId = process.env.REACT_APP_GOOGLE_OAUTH_KEY;
let auth;
export const authInit = () => (dispatch) => {
window.gapi.load('client:auth2', () =>
window.gapi.client.init({ clientId, scope: 'email' }).then(() => {
auth = window.gapi.auth2.getAuthInstance();
dispatch(changeSignedIn(auth.isSignedIn.get()));
auth.isSignedIn.listen((signedIn) => dispatch(changeSignedIn(signedIn)));
})
);
};
export const signIn = () => {
auth.signIn();
};
export const signOut = () => {
auth.signOut();
};
export const changeSignedIn = (signedIn) => {
const userId = signedIn ? auth.currentUser.get().getId() : null;
return {
type: SIGN_CHANGE,
payload: { signedIn, userId },
};
};
GoogleAuth.jsx
import { useSelector, useDispatch } from 'react-redux';
import classNames from 'classnames';
import { authInit, signIn, signOut } from '../../actions/authActions';
function GoogleAuth() {
const { signedIn } = useSelector((state) => state.auth);
const dispatch = useDispatch();
useEffect(() => {
dispatch(authInit());
}, [dispatch]);
const onClick = () => {
if (signedIn) {
signOut();
} else {
signIn();
}
};
let content;
if (signedIn === null) {
return null;
} else if (signedIn) {
content = 'Sign Out';
} else {
content = 'Sign In';
}
return (
<div className="item">
<button
className={classNames('ui google button', {
green: !signedIn,
red: signedIn,
})}
onClick={onClick}
>
<i className="ui icon google" />
{content}
</button>
</div>
);
}
export default GoogleAuth;
The code works fine, but it feels like it might be misleading having action calls in JSX but not dispatching it, is it okay?

Resources