I have a product page. I want to use SSG for all products. (about 1000 products).
but each page have related products and suggestion products I want make them with CSR.
this my code:
const initialState = {
relatedProducts: [] as LightGifttDto[],
suggestionsProducts: [] as LightGifttDto[],
}
interface Props {
gift: GiftDto;
}
export default function Gift(props: Props) {
const {gift} = props;
const [pageState, setPageState] = useState(initialState);
//to scroll down of any where
const commentRef = useRef(null)
//to get related products and suggestion products
useEffect(() => {
async function fetchData() {
const [relatedProducts, suggestionsProducts] = await Promise.all([
getRelatedProducts(gift.id),
getSuggestionsProducts(gift.id),
]);
setPageState({
relatedProducts,
suggestionsProducts,
})
}
fetchData();
}, [])
return (
<ShopLayout>
<GiftProvider value={gift}>
<PriceGiftProvider>
<ProductPage commentRef={commentRef}/>
</PriceGiftProvider>
<ProductCarouselSection
products={pageState.relatedProducts}
/>
<ProductComment commentRef={commentRef}/>
<ProductCarouselSection
products={pageState.suggestionsProducts}
/>
</GiftProvider>
</ShopLayout>
);
}
export async function getStaticPaths() {
const giftslugs = await getTopGiftsSlugForServerSideGenerate();
return { paths: giftslugs, fallback: "blocking" };
}
export async function getStaticProps({params}) {
const slug = encodeURI(params.slug);
const gift = await getGiftWithSlug(slug);
return { props: { gift } };
}
But I do not know this way is correct or not for to combine SSG and CSR.
Related
I have a NextJS application that is using the ShopifyBuy SDK. I have been successfully able to implement a solution where I am able to fetch the products from Store and display them to the User. The user is also able to go to a product page and add the product to the cart.
However, when the user refreshes the page, the cart is reset, and the data does not persist. The code is below:
context/cart.js:
import { createContext, useContext, useEffect, useReducer } from "react";
import client from "../lib/client";
import Cookies from "js-cookie";
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = "SET_CART";
const initalState = {
lineItems: [],
totalPrice: 0,
webUrl: "",
id: "",
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const savedState = Cookies.get("cartState");
const [state, dispatch] = useReducer(reducer, savedState || initalState);
useEffect(() => {
Cookies.set("cartState", state, { expires: 7 });
}, [state]);
useEffect(() => {
getCart();
}, []);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
const getCart = async () => {
try {
const cart = await client.checkout.create();
setCart(cart);
} catch (err) {
console.log(err);
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={{ state }}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
products/[handle].tsx:
import React, { useState, useEffect } from "react";
import client from "../../lib/client";
import { useCartDispatch, useCartState } from "../../context/cart";
import Link from "next/link";
import cookie from "js-cookie";
export const getStaticPaths = async () => {
const res = await client.product.fetchAll();
const paths = res.map((product: any) => {
return {
params: { handle: product.handle.toString() },
};
});
return {
paths,
fallback: false,
};
};
export const getStaticProps = async (context: any) => {
const handle = context.params.handle;
const res = await client.product.fetchByHandle(handle);
const product = JSON.stringify(res);
return {
props: {
product,
},
};
};
function Product({ product }: any) {
const { state } = useCartState();
const { setCart } = useCartDispatch();
const addToCart = async () => {
const checkoutId = state.id;
const lineItemsToAdd = [
{
variantId: product.variants[0].id,
quantity: 1,
},
];
const res = await client.checkout.addLineItems(checkoutId, lineItemsToAdd);
setCart(res);
};
product = JSON.parse(product);
return (
<div>
<div className=" flex-col text-2xl font-bold m-8 flex items-center justify-center ">
<h1>{product.title}</h1>
<button onClick={addToCart}>Add to Cart</button>
<Link href="/cart">Checkout</Link>
</div>
</div>
);
}
export default Product;
pages/cart/index.tsx:
import React, { useEffect } from "react";
import { useCartState, useCartDispatch } from "../../context/cart";
import client from "../../lib/client";
function Cart() {
const { state } = useCartState();
return (
<div>
<h1>Cart</h1>
{state.lineItems &&
state.lineItems.map((item: any) => {
return (
<div key={item.id}>
<h2>{item.title}</h2>
<p>{item.variant.title}</p>
<p>{item.quantity}</p>
</div>
);
})}
</div>
);
}
export default Cart;
I have tried using a library called js-cookie and also localStorage. I'm not sure where the problem lies or if the solutions that I've tried are wrong.
P.S.: I'm fairly new to NextJS and Typescript so go easy on the syntax. This code is for a personal project. Thanks in advance!
Answering this because I ended up coming up with a solution that works for me, at least.
Here it is:
const getCart = async () => {
try {
const checkoutId = Cookies.get("checkoutId");
let cart;
if (checkoutId) {
cart = await client.checkout.fetch(checkoutId);
} else {
cart = await client.checkout.create();
Cookies.set("checkoutId", cart.id);
}
setCart(cart);
} catch (err) {
console.log(err);
}
};
From my understanding, what this does is the following:
Check the cookies to see if one exists called "checkoutId"
If it exists, fetch the cart using that checkoutId
Otherwise, create a new cart and create a cookie using the cart.id that is returned in the response
Then, inside my individual Product page ([handle].tsx), I'm doing the following:
const addToCart = async () => {
const checkoutId = state.id;
const lineItemsToAdd = [
{
variantId: product.variants[0].id,
quantity: 1,
},
];
const res = await client.checkout.addLineItems(checkoutId, lineItemsToAdd);
console.log(res);
if (cookie.get("checkoutId") === undefined) {
cookie.set("checkoutId", res.id);
}
setCart(res);
};
Using cookies to store your object cart, as far as I know, is not a good idea. You could use localStorage, like so:
import { createContext, useContext, useEffect, useReducer } from "react";
import client from "../lib/client";
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = "SET_CART";
const initalState =
typeof localStorage !== "undefined" && localStorage.getItem("cartState")
? JSON.parse(localStorage.getItem("cartState"))
: {
lineItems: [],
totalPrice: 0,
webUrl: "",
id: "",
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initalState);
useEffect(() => {
localStorage.set("cartState", JSON.stringify(state));
}, [state]);
useEffect(() => {
getCart();
}, []);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
const getCart = async () => {
try {
const cart = await client.checkout.create();
setCart(cart);
} catch (err) {
console.log(err);
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={{ state }}>{children}</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
I am fetching data
I have a brand property and I am trying to display it, and I am trying to implement a click setCurrentSelectedBrand logic to diplay the data of that specific brand[object]. Currently I am getting the TypeError: Cannot read property 'brand' of undefined error in the Main Component, I dont know why
Data
[{
"a_class":[
{
"brand":"A-class",
"id":"1",
"year":"2015"
"price":"12665"
...
"engine_spec":{
...
}
...
}],
"b_class":[
{
"brand":"B-class",
"id":"2",
"year":"2016"
"price":"12665"
...
"engine_spec":{
...
}
...
}],
}
]
Main Component
const Cars = () => {
const { cars, handleSelectBrand} = useContext(CarsContext)
return (
<div>
{Object.keys(cars).map((key:any, index)=>{
let brands:any = cars[key];
return (
<div key={key} onClick={() => handleSelectBrand(key)} className='brand__list' >
{brands[0].brand}
</div>
);})}
<CarsDetails />
</div>
)
}
export default Cars
Context
declare module 'axios' {
export interface AxiosResponse<T = any> extends Promise<T> {}
}
export const CarsProvider:React.FC <IProps> = ({ children } ) => {
const [isLoading, setIsLoading] = useState(false);
const [cars, setCars] =useState< any | ICars[] >([])
const [brands, setBrands] = useState([])
const [currentSelectedBrand, setCurrentSelectedBrand] = useState('')
const handleSelectBrand = React.useCallback((brand) => {
return setCurrentSelectedBrand(cars[brand]);
},[cars])
useEffect(()=>{
const fetchData = async () => {
setIsLoading(true);
const response = await api.get('/cars', {
});
setIsLoading(false)
setCars([...response.data]);
setCurrentSelectedBrand(response[Object.keys(response)[0]]);
console.log(response.data)
};
fetchData()
},[brands, cars])
In many components, I need to fetch some data and I'm ending up with a lot of similar code. It looks like this:
const [data, setData] = useState();
const [fetchingState, setFetchingState] = useState(FetchingState.Idle);
useEffect(
() => {
loadDataFromServer(props.dataId);
},
[props.dataId]
);
async function loadDataFromServer(id) {
let url = new URL(`${process.env.REACT_APP_API}/data/${id}`);
let timeout = setTimeout(() => setFetchingState(FetchingState.Loading), 1000)
try {
const result = await axios.get(url);
setData(result.data);
setFetchingState(FetchingState.Idle);
}
catch (error) {
setData();
setFetchingState(FetchingState.Error);
}
clearTimeout(timeout);
}
How can I put it into a library and reuse it?
Thank you guys for the suggestion, I came up with the following hook. Would be happy to some critics.
function useFetch(id, setData) {
const [fetchingState, setFetchingState] = useState(FetchingState.Idle);
useEffect(() => { loadDataFromServer(id); }, [id]);
async function loadDataFromServer(id) {
let url = new URL(`${process.env.REACT_APP_API}/data/${id}`);
let timeout = setTimeout(() => setFetchingState(FetchingState.Loading), 1000)
try {
const result = await axios.get(url);
setData(result.data);
setFetchingState(FetchingState.Idle);
}
catch (error) {
setData();
setFetchingState(FetchingState.Error);
}
clearTimeout(timeout);
}
return fetchingState;
}
And this is how I use it:
function Thread(props) {
const [question, setQuestion] = useState();
const fetchingState = useFetch(props.questionId, setQuestion);
if (fetchingState === FetchingState.Error) return <p>Error while getting the post.</p>;
if (fetchingState === FetchingState.Loading) return <Spinner />;
return <div>{JSON.stringify(question)}</div>;
}
You can wrap your APIs calls in /services folder and use it anywhere
/services
- Auth.js
- Products.js
- etc...
Example
Auth.js
import Axios from 'axios';
export const LoginFn = (formData) => Axios.post("/auth/login", formData);
export const SignupFn = (formData) => Axios.post("/auth/signup", formData);
export const GetProfileFn = () => Axios.get("/auth/profile")
in your component
import React, { useState } from 'react'
import { LoginFn } from '#Services/Auth'
export LoginPage = () => {
const [isLoading, setIsLoading] = useState(false);
const LoginHandler = (data) => {
setIsLoading(true)
LoginFn(data).then(({ data }) => {
// do whatever you need
setIsLoading(false)
})
}
return (
<form onSubmit={LoginHandler}>
.......
)
}
I'm trying to render a header.
First, in InnerList.js, I make an API call, and with the data from the API call, I set a list in context.
Second, in Context.js, I take the list and set it to a specific data.
Then, in InnerListHeader.js, I use the specific data to render within the header.
Problem: I currently get a TypeError undefined because the context is not set before rendering. Is there a way to wait via async or something else for the data to set before loading?
My code block is below. I've been looking through a lot of questions on StackOverflow and blogs but to no avail. Thank you!
InnerList.js
componentDidMount() {
const { dtc_id } = this.props.match.params;
const {
setSpecificDtcCommentList,
} = this.context;
MechApiService.getSpecificDtcCommentList(dtc_id)
.then(res =>
setSpecificDtcCommentList(res)
)
}
renderSpecificDtcCommentListHeader() {
const { specificDtc = [] } = this.context;
return (
<InnerDtcCommentListItemHeader key={specificDtc.id} specificDtc={specificDtc} />
)
}
Context.js
setSpecificDtcCommentList = (specificDtcCommentList) => {
this.setState({ specificDtcCommentList })
this.setSpecificDtc(specificDtcCommentList)
}
setSpecificDtc = (specificDtcCommentList) => {
this.setState({ specificDtc: specificDtcCommentList[0] })
}
InnerListHeader.js
render() {
const { specificDtc } = this.props;
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{specificDtc.dtc_id.dtc}
</div>
</div>
);
}
In general, you should always consider that a variable can reach the rendering stage without a proper value (e.g. unset). It is up to you prevent a crash on that.
For instance, you could rewrite you snippet as follows:
render() {
const { specificDtc } = this.props;
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{Boolean(specificDtc.dtc_id) && specificDtc.dtc_id.dtc}
</div>
</div>
);
}
When you make an api call you can set a loader while the data is being fetched from the api and once it is there you show the component that will render that data.
In your example you can add a new state that will pass the api call status to the children like that
render() {
const { specificDtc, fetchingData } = this.props;
if (fetchingData){
return <p>Loading</p>
}else{
return (
<div>
<div className="InnerDtcCommentListItemHeader__comment">
{specificDtc.dtc_id.dtc}
</div>
</div>
);
}
}
``
in my case, i am calling external api to firebase which lead to that context pass undefined for some values like user. so i have used loading set to wait untile the api request is finished and then return the provider
import { createContext, useContext, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
GoogleAuthProvider,
signInWithPopup,
updateProfile
} from 'firebase/auth';
import { auth } from '../firebase';
import { useNavigate } from 'react-router';
import { create_user_db, get_user_db } from 'api/UserAPI';
import { CircularProgress, LinearProgress } from '#mui/material';
import Loader from 'ui-component/Loader';
const UserContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState();
const [user_db, setUserDB] = useState();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const createUser = async (email, password) => {
const user = await createUserWithEmailAndPassword(auth, email, password);
};
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password)
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
};
const googleSignIn = async () => {
const provider = new GoogleAuthProvider();
await signInWithPopup(auth, provider)
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false));
};
const logout = () => {
setUser();
return signOut(auth).then(() => {
window.location = '/login';
});
};
const updateUserProfile = async (obj) => {
await updateProfile(auth.currentUser, obj);
return updateUser(obj);
};
const updateUser = async (user) => {
return setUser((prevState) => {
return {
...prevState,
...user
};
});
};
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
setLoading(true);
if (currentUser) {
const user_db = await get_user_db({ access_token: currentUser.accessToken });
setUserDB(user_db);
setUser(currentUser);
setIsAuthenticated(true);
}
setLoading(false);
});
return () => {
unsubscribe();
};
}, []);
if (loading) return <Loader />;
return (
<UserContext.Provider value={{ createUser, user, user_db, isAuthenticated, logout, signIn, googleSignIn, updateUserProfile }}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};
I am making dummy app to test server side API.
First request returns nested JSON object with Product names and number of variants that it has. From there I extract Product name so I can send second request to fetch list of variants with product images, sizes etc.
Sometimes it will load and display variants from only one product but most of the times it will work correctly and load all variants from both dummy products.
Is there a better way of doing this to ensure it works consistently good. Also I would like to know if there is a better overall approach to write something like this.
Here is the code:
import React, { useEffect, useState } from "react";
import axios from "axios";
import ShirtList from "../components/ShirtList";
const recipeId = "15f09b5f-7a5c-458e-9c41-f09d6485940e";
const HomePage = props => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
axios
.get(
`https://api.print.io/api/v/5/source/api/prpproducts/?recipeid=${recipeId}&page=1`
)
.then(response => {
let shirtList = [];
const itemsLength = response.data.Products.length;
response.data.Products.forEach((element, index) => {
axios
.get(
`https://api.print.io/api/v/5/source/api/prpvariants/?recipeid=${recipeId}&page=1&productName=${element.ProductName}`
)
.then(response => {
shirtList.push(response.data.Variants);
if (index === itemsLength - 1) {
setLoaded(shirtList);
}
});
});
});
}, []);
const ListItems = props => {
if (props.loaded) {
return loaded.map(item => <ShirtList items={item} />);
} else {
return null;
}
};
return (
<div>
<ListItems loaded={loaded} />
</div>
);
};
export default HomePage;
You are setting the loaded shirts after each iteration so you will only get the last resolved promise data, instead fetch all the data and then update the state.
Also, separate your state, one for the loading state and one for the data.
Option 1 using async/await
const recipeId = '15f09b5f-7a5c-458e-9c41-f09d6485940e'
const BASE_URL = 'https://api.print.io/api/v/5/source/api'
const fetchProducts = async () => {
const { data } = await axios.get(`${BASE_URL}/prpproducts/?recipeid=${recipeId}&page=1`)
return data.Products
}
const fetchShirts = async productName => {
const { data } = await axios.get(
`${BASE_URL}/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`,
)
return data.Variants
}
const HomePage = props => {
const [isLoading, setIsLoading] = useState(false)
const [shirtList, setShirtList] = useState([])
useEffect(() => {
setIsLoading(true)
const fetchProductShirts = async () => {
const products = await fetchProducts()
const shirts = await Promise.all(
products.map(({ productName }) => fetchShirts(productName)),
)
setShirtList(shirts)
setIsLoading(false)
}
fetchProductShirts().catch(console.log)
}, [])
}
Option 2 using raw promises
const recipeId = '15f09b5f-7a5c-458e-9c41-f09d6485940e'
const BASE_URL = 'https://api.print.io/api/v/5/source/api'
const fetchProducts = () =>
axios.get(`${BASE_URL}/prpproducts/?recipeid=${recipeId}&page=1`)
.then(({ data }) => data.Products)
const fetchShirts = productName =>
axios
.get(
`${BASE_URL}/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`,
)
.then(({ data }) => data.Variants)
const HomePage = props => {
const [isLoading, setIsLoading] = useState(false)
const [shirtList, setShirtList] = useState([])
useEffect(() => {
setIsLoading(true)
fetchProducts
.then(products) =>
Promise.all(products.map(({ productName }) => fetchShirts(productName))),
)
.then(setShirtList)
.catch(console.log)
.finally(() => setIsLoading(false)
}, [])
}
Now you have isLoading state for the loading state and shirtList for the data, you can render based on that like this
return (
<div>
{isLoading ? (
<span>loading...</span>
) : (
// always set a unique key when rendering a list.
// also rethink the prop names
shirtList.map(shirt => <ShirtList key={shirt.id} items={shirt} />)
)}
</div>
)
Refferences
Promise.all
Promise.prototype.finally
React key prop
The following should pass a flat array of all variants (for all products ) into setLoaded. I think this is what you want.
Once all the products have been retrieved, we map them to an array of promises for fetching the variants.
We use Promise.allSettled to wait for all the variants to be retrieved, and then we flatten the result into a single array.
useEffect(()=>(async()=>{
const ps = await getProducts(recipeId)
const variants = takeSuccessful(
await Promise.allSettled(
ps.map(({ProductName})=>getVariants({ recipeId, ProductName }))))
setLoaded(variants.flat())
})())
...and you will need utility functions something like these:
const takeSuccessful = (settledResponses)=>settledResponses.map(({status, value})=>status === 'fulfilled' && value)
const productURL = (recipeId)=>`https://api.print.io/api/v/5/source/api/prpproducts/?recipeid=${recipeId}&page=1`
const variantsURL = ({recipeId, productName})=>`https://api.print.io/api/v/5/source/api/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`
const getProducts = async(recipeId)=>
(await axios.get(productURL(recipeId)))?.data?.Products
const getVariants = async({recipeId, productName})=>
(await axios.get(variantsURL({recipeId,productName})))?.data?.Variants