infinite scroll with get static props and grapql not working - next.js

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}}>

Related

3 levels nested dynamic routes with 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

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!

How to add Spinner at specific route?

I get this code in question:nextjs getServerSideProps show loading
import Router from "next/router";
export default function App({ Component, pageProps }) {
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
const start = () => {
console.log("start");
setLoading(true);
};
const end = () => {
console.log("findished");
setLoading(false);
};
Router.events.on("routeChangeStart", start);
Router.events.on("routeChangeComplete", end);
Router.events.on("routeChangeError", end);
return () => {
Router.events.off("routeChangeStart", start);
Router.events.off("routeChangeComplete", end);
Router.events.off("routeChangeError", end);
};
}, []);
return (
<>
{loading ? (
<h1>Loading...</h1>
) : (
<Component {...pageProps} />
)}
</>
);
}
It work for me, but with all route, I just want to work with some route, and route have dynamic query, How can I do it?
First of all, you need to define a list of routes that you don't want to have a loading state. For example
//`/details/` can be a dynamic route like `/details/1`, `/details/2`, etc.
const EXCEPTED_ROUTES = ['/details/'] //routes based on definitions
And then you can do it with URL param in routeChangeStart event
const start = (url) => {
const isURLMatched = EXCEPTED_ROUTES.some(exceptedRoute => url.startsWith(exceptedRoute))
if(isURLMatched) { //the condition here is your choice
return //should not set loading state
}
console.log("start");
setLoading(true);
};

TypeError: dispatch is not a function when clicking the toggle button

I am using react redux-thunk. I have a set of users data that I get from an API and this is the schema:
.
I've connected the "active" property with the checked attribute of a Switch MUI button, so naturally when calling the API I have some users with their switch button already on "true". What I am trying to do is to just make the switch functional, and just be able to click it and change its state, not necessarily doing anything with that.
Here's my toggleType.js:
export const TOGGLE = "TOGGLE";
Here's my toggleAction.js:
import { TOGGLE } from "./toggleType";
const statusToggleAction = () => {
return {
type: TOGGLE,
};
};
export const statusToggle = () => {
return (dispatch) => {
dispatch(statusToggleAction);
};
};
Here's my toggleReducer.js:
import { TOGGLE } from "./toggleType";
const initialState = {
status: false,
};
const toggleReducer = (state = initialState, action) => {
switch (action.type) {
case TOGGLE:
status: true;
default:
return state;
}
};
export default toggleReducer;
Everything is under my userContainer.js, like that:
function UserContainer({ userData, fetchUsers }) {
useEffect(() => {
fetchUsers();
}, []);
return userData.loading ? (
<h2>Loading</h2>
) : userData.error ? (
<h2>{userData.error}</h2>
) : (
<Container maxWidth="lg" style={{ flexGrow: 1, height: "100%" }}>
<h2>User List</h2>
<div>
{userData &&
userData.users &&
userData.users.map((user) => (
<div key={user.id}>
<p>{user.name}</p>
<Switch checked={user.active} onChange={statusToggle()} />
</div>
))}
</div>
</Container>
);
}
const mapStateToProps = (state) => {
return { userData: state.user, statusToggle: state.status };
};
const mapDispatchToProps = (dispatch) => {
return {
fetchUsers: () => dispatch(fetchUsers()),
statusToggle: () => dispatch(statusToggle()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(UserContainer);
This is the error I am getting whenever I am clicking one of those switches:
Any ideas are welcome, I "learned" redux like 3 days ago!
toggleReducer function in toggleReducer.js, replace status: true; with return { status: true }.
Just return action in statusToggle function in toggleAction.js without dispatch as following.
export const statusToggle = () => {
return statusToggleAction();
};
Or just call statusToggleAction directly in userContainer.js as following.
export const statusToggle = () => {
return (dispatch) => {
dispatch(statusToggleAction());
};
};

Pass React.Context to Nextjs after ComponentDidMount?

I have an issue where I have a simple React.Context that's populated after all the components mount. The problem is that because it happens after mount, nextjs does not see this data on initial render, and so there's noticeable flicker.
Here's the simple component that sets the Context:
export const SetTableOfContents = (props: { item: TableOfContentsItem }) => {
const toc = useContext(TableOfContentsContext);
useEffect(() => {
// Updates the React.Context after the component mount
// (since useEffects run after mount)
toc.setItem(props.item);
}, [props.item, toc]);
return null;
};
Here's the React.Context. It uses React state to store the TOC items.
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const [items, setItems] = useState<TableOfContents["items"]>([]);
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
setItems((items) => items.concat(item));
},
};
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
Currently, it is not possible to set the React.Context before mount because React gives a warning---Cannot update state while render.
The only workaround I can think of is to use something other than React.state for the React.Context state---that way the component can update it any time it wants. But then the problem with that approach is that Context Consumers would no longer know that the items changed (because updates live outside the React lifecycle)!
So how to get the initial React.Context into the initial SSR render?
const items = [];
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
items[item.index] = item;
},
};
// this dep never changes.
// when you call this function, values never change
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
Here's what I ended up doing:
render the app in getStaticProps using renderToString
use useRef for state in the Context instead of useState
the reason for doing this is because renderToString renders only the initial state. So if you update the Context using useState, it won't capture subsequent renders
update the Context on component initialization for the reason mentioned above
pass the Context an "escape hatch"---a function we can call to get the state calculated on the initial render
Yes, the whole thing seems like a giant hack! :-) I'm not sure if React.Context plays well with SSR :(
export const TableOfContentsProvider = (props: {
initialItems?: TableOfContentsItem[];
setItemsForSSR?: (items: TableOfContentsItem[]) => void;
children?: React.ReactNode;
}) => {
// use useRef for the reasons mentioned above
const items = useRef(props.initialItems || []);
// Client still needs to see updates, so that's what this is for
const [count, setCount] = useState(0);
const { setItemsForSSR } = props;
const setterValue = useMemo(
() => ({
setItem(item: TableOfContentsItem) {
if (!items.current.find((x) => x.id === item.id)) {
items.current.push(item);
items.current.sort((a, b) => a.index - b.index);
setCount((count) => count + 1);
setItemsForSSR?.(items.current);
}
},
}),
[setItemsForSSR]
);
const stateValue = useMemo(() => ({ items: items.current, count }), [count]);
return (
<TableOfContentsSetterContext.Provider value={setterValue}>
<TableOfContentsStateContext.Provider value={stateValue}>
{props.children}
</TableOfContentsStateContext.Provider>
</TableOfContentsSetterContext.Provider>
);
};
interface TableOfContentsSetterWorkerProps {
item: TableOfContentsItem;
setItem: (item: TableOfContentsItem) => void;
}
export class TableOfContentsSetterWorker extends React.Component<
TableOfContentsSetterWorkerProps,
{}
> {
constructor(props: TableOfContentsSetterWorkerProps) {
super(props);
// Need to do this on init otherwise renderToString won't record it
props.setItem(props.item);
}
render() {
return null;
}
}
/**
* Usage: use this as a child component when the parent needs to set the TOC.
*
* Exists so that a component can set the TOC without triggering
* an unnecessary render on itself.
*/
export function TableOfContentsSetter(props: { item: TableOfContentsItem }) {
const { setItem } = useContext(TableOfContentsSetterContext);
return <TableOfContentsSetterWorker item={props.item} setItem={setItem} />;
export const getStaticProps = async () => {
let initialTableOfContents: TableOfContentsItem[] = [];
const getItems = (items: TableOfContentsItem[]) => {
initialTableOfContents = [...items];
};
const app = () => (
<TableOfContentsProvider setItemsForSSR={getItems}>
<AppArticles />
</TableOfContentsProvider>
);
renderToString(app());
return {
props: {
initialTableOfContents,
},
};
};

Resources