Apollo GraphQL appends duplicates to component state - javascript

I have a page that contains a component that renders a list from the results of a query. When I load the page the first time, the list renders fine. But whenever I go to another page and navigate back, an additional set of the result is appended to the list, creating duplicates in the DOM.
I'm not sure what I'm doing wrong here, but I don't want a new set items to be appended to the list every time I load the page.
apolloClient (https://github.com/vercel/next.js/tree/canary/examples/with-apollo)
let apolloClient;
const createApolloClient = () =>
new ApolloClient({
ssrMode: typeof window === "undefined",
link: new HttpLink({
uri: DB_URI,
credentials: "same-origin",
}),
cache: new InMemoryCache(),
});
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient();
if (initialState) {
const existingCache = _apolloClient.extract();
const data = merge(initialState, existingCache);
_apolloClient.cache.restore(data);
}
if (typeof window === "undefined") return _apolloClient;
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function addApolloState(client, pageProps) {
if (pageProps?.props) {
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
}
return pageProps;
}
export function useApollo(pageProps) {
const state = pageProps[APOLLO_STATE_PROP_NAME];
return useMemo(() => initializeApollo(state), [state]);
}
On my page I use getStaticProps as follows
export async function getStaticProps() {
const apolloClient = initializeApollo();
await apolloClient.query({
query: GET_THINGS,
});
return addApolloState(apolloClient, {
props: {},
revalidate: 1,
});
}
My list component looks as follows:
const ItemsList: React.FunctionComponent<Props> = (props) => {
const { loading, error, data } = useQuery(GET_THINGS, {});
const { items} = data;
const { filters } = props;
const [filteredItems, setFilteredItems] = useState(items);
useEffect(() => {
setFilteredItems(filterItems(items, filters));
}, [filters, items]);
const renderItems = (filteredItems: Array<Item>) =>
filteredItems.map((item) => (
<li key={item.id}>
<Link href={`/items/${item.id}`}>{item.name}</Link>
</li>
));
if (loading) return <div>"Loading...";</div>;
if (error) return <div>`Error! ${error.message}`;</div>;
return (
<div>
{filteredItems?.length > 0 ? (
<ul>{renderItems(filteredItems)}</ul>
) : (
<span>No items matched the criteria</span>
)}
</div>
);
};
export default ItemsList;

Related

How can I make the data inside of my Cart become persistent?

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);

How to implement cleanup function in useEffect while fetching data from API in contex provider

I want to display a list of products based on specific categories fetched from api, like below:
const API = "https://dummyjson.com/products";
const ProductsList = () => {
const { cate } = useParams(); //here I am getting category from Viewall component
const { getFilterProducts, filter_products } = useFilterContext();
useEffect(() => {
getFilterProducts(`${API}/category/${cate}`);
}, [cate]);
return (
<div className="mx-2 mt-2 mb-16 md:mb-0 grid grid-cols-1 md:grid-cols-12">
<div className="h-9 w-full md:col-span-2">
<FilterSection />
</div>
<div className="md:col-span-10">
<ProductListDetails products={filter_products} />
</div>
</div>
);
};
My FilterContextProvider is as follows
const initialState = {
filter_products: [],
};
const FilterProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const { products } = useAppContext();
const getFilterProducts = async (url) => {
dispatch({ type: "FILTERS_LOADING" });
try {
const res = await fetch(url);
const data = await res.json();
if (!res.ok) {
var error = new Error("Error" + res.status + res.statusText);
throw error;
}
dispatch({ type: "LOAD_FILTER_PRODUCTS", payload: data.products });
} catch (err) {
dispatch({ type: "FILTERS_ERROR", payload: err.message });
}
};
return (
<FilterContext.Provider value={{ ...state, getFilterProducts }}>
{children}
</FilterContext.Provider>
);
};
I tried using this simple approach in my ProductList component to clean up:
useEffect(() => {
let inView = true;
getFilterProducts(`${API}/category/${cate}`);
return () => {
inView = false;
};
}, [cate]);
But it does not seem to work. When I move to the ProductList component, it first displays data of my previous filer_products value, then after a few fractions of seconds, updates the data and shows current data.
I am expecting that when the ProductList component unmounts, its rendered data should vanish, and when I navigate it again, it should render the current data directly, not after a fraction of seconds.
As you explained, I assume your context is wrapping your routes, and it's not re-rendering when switching between pages. A simple solution is to have a loader in ProductsList, wait for the new data to replace the old, and have the user notice what's happening with a loader:
const ProductsList = () => {
const { cate } = useParams(); //here I am getting category from Viewall component
const { getFilterProducts, filter_products } = useFilterContext();
const [loading, setLoading] = useState(true);
useEffect(() => {
getFilterProducts(`${API}/category/${cate}`).then(() => {
setLoading(false);
});
}, [cate]);
if (loading) {
return <p>Hang tight, the data is being fetched...</p>;
}
return (
<div className="mx-2 mt-2 mb-16 md:mb-0 grid grid-cols-1 md:grid-cols-12">
<div className="h-9 w-full md:col-span-2">
<FilterSection />
</div>
<div className="md:col-span-10">
<ProductListDetails products={filter_products} />
</div>
</div>
);
};
If you need to clear your store in a clean-up function, you can add dispatch as part of your context value, grab it in ProductsList and call it like so:
<FilterContext.Provider value={{ ...state, getFilterProducts, dispatch }}>
{children}
</FilterContext.Provider>
const { getFilterProducts, filter_products, dispatch } = useFilterContext();
useEffect(() => {
getFilterProducts(`${API}/category/${cate}`);
return () => {
dispatch({ type: "LOAD_FILTER_PRODUCTS", payload: {} });
};
}, [cate]);

trying to use crawled data but error appears like 'Objects are not valid as a React child (found: [object Promise])'

I want to print out the crawled data from the site I want using Gatsby. But I don't know why this error appears.
here's my crawler
class Crawler {
constructor() {
this.client = axios.create();
}
async crawlNews() {
const url = 'https://finance.naver.com/news/news_list.naver?mode=RANK';
const settedResult = await this.client
.get(url, { responseType: 'arraybuffer' })
.then((response) => {
const setResult = [];
const content = iconv.decode(response.data, 'EUC-KR');
const $ = cheerio.load(content);
$('.simpleNewsList > li').each((i, el) => {
const title = $(el).text();
setResult.push({
id: parseInt(i) + 1,
title: title
.replace(/(\r\n|\n|\r|\t)/gm, '')
.toString(),
});
});
return setResult;
})
.catch((err) => console.error(err));
return settedResult;
}
}
and here's Slide component
import React from 'react';
export function Slide(props) {
const { index, title } = props;
return (
<div>
{index} | {title}
</div>
);
}
here's pages/index.js in gatsby
async function Home() {
const settedResult = new Crawler();
const dataSource = await settedResult.crawlNews();
const result = dataSource.map((obj) => {
<Slide index={obj.id} title={obj.title} />;
});
return <div>{result}</div>;
}
export default Home;
When I run 'gatsby develop' with the above files, an error like the title appears
Maybe you can provide a stackblitz ? Its seems you'r missing a return here :
const result = dataSource.map((obj) => {
<Slide index={obj.id} title={obj.title} />;
});
This should work
const result = dataSource.map((obj) =>
<Slide index={obj.id} title={obj.title} />
);
or
const result = dataSource.map((obj) => {
return <Slide index={obj.id} title={obj.title} />;
};

Auto unsubscribe from custom store

I am developing app using svelte and I use custom stores.
I know that svelte automatically handles unsubscribe for us if we use $
<h1>The count is {$count}</h1>
but, as I have to filter my array in script, how can I use this advantage?
from
const filteredMenu = menu.filter("header");
to
$: filteredMenu = menu.filter("header");
?. or maybe I have to manually unsubscribe on unMount hook?
I am including my code
// /store/menu.ts
import { writable } from "svelte/store";
import { myCustomFetch } from "#/utils/fetch";
import type { NavbarType } from "#/types/store/menu";
const createMenu = () => {
const { subscribe, set } = writable(null);
let menuData: Array<NavbarType> = null;
return {
subscribe,
fetch: async (): Promise<void> => {
const { data, success } = await myCustomFetch("/api/menu/");
menuData = success ? data : null;
},
sort(arr: Array<NavbarType>, key: string = "ordering") {
return arr.sort((a: NavbarType, b: NavbarType) => a[key] - b[key]);
},
filter(position: string, shouldSort: boolean = true) {
const filtered = menuData.filter((item: NavbarType) =>
["both", position].includes(item.position)
);
return shouldSort ? this.sort(filtered) : filtered;
},
reset: () => set(null),
};
};
export const menu = createMenu();
// Navbar.svelte
<sript>
const filteredMenu = menu.filter("header");
</script>
{#each filteredMenu as item, index (index)}
<a
href={item.url}
target={item.is_external ? "_blank" : null}
class:link-selected={activeIndex == index}>{item.title}
</a>
{/each}

Displaying response from API in react component

I'm trying to display the response from the API into my react component but it's not working. If I try to use it in the console, I can see the data and its value but not in the react component, it's empty when I try to show the value in a div.
Here is the code where I'm trying to display it in my react component:
const CharacterListing = () => {
const characters = useSelector(getAllCharacters);
console.log("Hello", characters);
const renderCharacters = Object.entries(characters).map(([key, value]) => {
console.log(value.name);
<div>{value.name}</div>
})
return (
<div>
{renderCharacters}
</div>
);
};
export default CharacterListing;
This is the code for my Character Slice Component
const initialState = {
characters: {},
};
const characterSlice = createSlice({
name: 'characters',
initialState,
reducers: {
addCharacters: (state, { payload }) => {
state.characters = payload;
},
},
});
export const { addCharacters } = characterSlice.actions;
export const getAllCharacters = (state) => state.characters.characters;
export default characterSlice.reducer;
This is the code for my Home Component:
const Home = () => {
const dispatch = useDispatch();
useEffect(() => {
const fetchCharacters = async () => {
const response = await baseURL.get(`/characters`)
.catch(error => {
console.log("Error", error);
});
dispatch(addCharacters(response.data));
console.log("Success", response);
};
fetchCharacters();
}, [])
return (
<div>
Home
<CharacterListing />
</div>
);
};
export default Home;
Thank you
You forgot to return item into your map func
Try this :
const renderCharacters = Object.entries(characters).map(([key, value]) => {
console.log(value.name);
return <div key={key}>{value.name}</div>
})

Categories