React-Redux: Changes in state by different reducer - redux

I am building this little project of a vending machine and I am having problems because two different parts of my state change from a reducer that should only change one part. Here are the code snippets :
//Action creators
export const buyProduct = product => {
return {
type: 'BUY_PRODUCT',
payload: product
};
};
export const showProducts = () => {
return {
type: 'SHOW_PRODUCTS',
};
};
export const addCredit = amount => {
return {
type: 'ADD_MONEY',
payload: amount
};
};
export const refillVendingMachine = () => {
return {
type: 'REFILL_VENDING_MACHINE'
};
};
//Reducers
import { combineReducers } from 'redux';
const data = [
{code: '110', price: 10, name: 'KitKat', quantity: 5, image: 'https://i.ibb.co/41dKJ8r/kitkat.jpg'},
{code: '111', price: 11, name: 'Nuggets', quantity: 5, image: 'http://lorempixel.com/135/100/food'},
{code: '112', price: 12, name: 'Mars', quantity: 5, image: 'http://lorempixel.com/135/100/food'},
{code: '113', price: 13, name: 'Twix', quantity: 5, image: 'http://lorempixel.com/135/100/food'},
{code: '114', price: 14, name: '7days', quantity: 5, image: 'http://lorempixel.com/135/100/food'},
{code: '115', price: 15, name: 'Water', quantity: 5, image: 'http://lorempixel.com/135/100/food'},
{code: '210', price: 14, name: '7days', quantity: 5, image: 'http://lorempixel.com/135/100/food'},
{code: '211', price: 14, name: '7days', quantity: 5, image: 'http://lorempixel.com/135/100/food'},
{code: '212', price: 14, name: '7days', quantity: 5, image: 'http://lorempixel.com/135/100/food'}
];
const productsReducer = (products = data, action) => {
if(action.type === 'BUY_PRODUCT') {
return products.map(product => {
if(product.code === action.payload.code) {
if(product.quantity === 1) {
product.quantity--;
product.image = "https://i.ibb.co/w0sSgpd/sold-out.png";
return product;
};
product.quantity--
return product;
};
return product;
});
}
return products;
};
const moneyReducer = (state = 0, action) => {
if(action.type === 'BUY_PRODUCT') {
return state - parseInt(action.payload.price);
} else if(action.type === "ADD_MONEY") {
return state + parseInt(action.payload);
}
return state;
};
const boughtProductsReducer = (state = [], action) => {
if(action.type === 'BUY_PRODUCT') {
if(state.length === 3){
state.shift();
}
return [...state, action.payload]
};
return state;
};
export default combineReducers({
products: productsReducer,
money: moneyReducer,
boughtProducts: boughtProductsReducer
});
And as you would encounter in a vending machine when a product's quantity becomes 0 it should show sold out only in the products state but it also changes the image in the boughtProducts state.
Any idea why? and how to fix it?
Update:
Here is where I display all my products and where I need the image to change
import React from 'react';
import './Rows.css';
import './Product.css';
import { connect } from 'react-redux';
class Rows extends React.Component {
renderRows = () => {
return this.props.products.map(product => {
return (
<div className="this">
<div className="product" key={product.code}>
<div className="body">
<div className="title">
{product.name}
</div>
<div className="image">
<img alt="blablabla" src={product.image} />
</div>
</div>
<div className="extra">
<span className="text">Q: {product.quantity}</span>
</div>
</div>
<div className="content">
<span className="text">#{product.code}</span>
<span className="text">{product.price} $</span>
</div>
</div>
);
});
};
render() {
return (
<React.Fragment>
{this.renderRows()}
</React.Fragment>
);
};
};
const mapStateToProps = state => {
console.log(state);
return {products: state.products};
};
export default connect(mapStateToProps)(Rows);
And here is where I store the bought products where the image should remain the same
import React from 'react';
import { connect } from 'react-redux';
import './Bin.css';
class Bin extends React.Component {
renderBin = () => {
console.log(this.props.boughtProducts);
return this.props.boughtProducts.map(product => {
return <div className="product" key={product.code*Math.random()}>
<div className="body">
<div className="title">
{product.name}
</div>
<div className="image">
<img alt="blablabla" src={product.image} />
</div>
</div>
</div>
});
};
render() {
return (
<div className="bin">
<i className="fas fa-angle-right fa-9x color"></i>
<div className="product-bin">
{this.renderBin()}
</div>
</div>
);
};
};
const mapStateToProps = state => {
return {boughtProducts: state.boughtProducts};
};
export default connect(mapStateToProps)(Bin);

const productsReducer = (products = data, action) => {
if(action.type === 'BUY_PRODUCT') {
return products.map(product => {
if(product.code === action.payload.code) {
if(product.quantity === 1) {
product.quantity--;
product.image = "https://i.ibb.co/w0sSgpd/sold-out.png";
return product;
};
product.quantity--
return product;
};
return product;
});
}
return products;
};
This part of the code changes the image url, and the products page is also using the same image. So, the change is reflected there as well. You can fix it by conditionally rendering like this
<img alt="blablabla" src={product.quantity === 0 ? `sold-out-image-path`: product.image} />
This will work fine if you are using the same sold out image for all the products. If the soldout image depends on the product, then you can add a property for that inside data[].
{code: '110', price: 10, name: 'KitKat', quantity: 5, image: 'https://i.ibb.co/41dKJ8r/kitkat.jpg', soldOutImage: 'product-specific-sold-out-image'},
and then write the UI code like this
<img alt="blablabla" src={product.quantity === 0 ? product.soldOutImage: product.image} />
I hope i was helpful :-)

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!

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

How to use getStaticProps or getServerSiteProps in Next.js to deal with object in local file?

Data are located in the local file: ./public/clients.js
How to use getStaticProps or getServerSideProps to deal with object in local file? How to write getStaticProps or getServerSideProps syntax?
export const users = new Promise((resolve) =>
resolve([
{
id: 1,
firstName: "Bob",
lastName: "Dole"
},
{
id: 2,
firstName: "Ann",
lastName: "Pell"
},
{
id: 3,
firstName: null,
lastName: "Wess"
},
])
);
When we use clinets.js file as is we will get the error:
After modified clients.js:
export const users = [
{
id: 1,
firstName: "Bob",
lastName: "Dole",
},
{
id: 2,
firstName: "Ann",
lastName: "Pell",
},
{
id: 3,
firstName: null,
lastName: "Wess",
},
];
example2.js (home page) I used in this case a little bit of tailwindcss:
import ItemsList from "../components/ItemsList";
import { users } from "../public/clients";
function example2({ data }) {
console.log(data);
return (
<div className="flex h-screen items-center justify-center flex-col gap-1">
<h1 className="text-3xl text-green-700 font-bold">
getStaticProps - example
</h1>
<ItemsList data={data} />
</div>
);
}
export const getStaticProps = async () => {
return {
props: {
data: { users },
},
};
};
export default example2;
ItemsList.js component:
import Items from "./Items";
function ItemsList({ data: { users } }) {
return (
<>
<ul>
{users.map((event) => (
<Items
key={event.id}
id={event.id}
firstName={event.firstName}
lastName={event.lastName}
/>
))}
</ul>
</>
);
}
export default ItemsList;
Items.js component:
function Items({ id, firstName, lastName }) {
return (
<div>
<small>{id}</small>
<h1>{firstName}</h1>
<h2>{lastName}</h2>
</div>
);
}
export default Items;
Output:
Tested with: "next": "12.0.7", "react": "17.0.2", "tailwindcss": "^3.0.5"

cannot return initial state of products in component - Redux

While I have an array of items as initialState:
export default {
cart: [],
products: [
{
id: 1,
name: "Redux",
price: 100000
},
{
id: 2,
name: "React",
price: 0
},
{
id: 3,
name: "Redux DevTools",
price: 10
}
]
}
I am accessing it through products-reducer:
import initialState from './initialState.js';
export default function products(state = initialState.products, action){
return state;
};
and the Products component which maps a list of products through the products-reducer :
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addToCart } from '../actions/cart-actions.js';
class Products extends Component {
render() {
const productList = this.props.products.map( (item,index) => {
return <div key={index}>
<p style={{ color: "#767676"}}>{item.name} - {item.price} $ </p>
<button className="button"
onClick={() => this.props.addToCart(item)}>
Add To Cart
</button>
</div>
});
return (
<div className= "products">
{ productList }
</div>
);
}
}
function mapStateToProps(state, props) {
return {
products: state.products
};
}
function mapDispatchToProps(dispatch) {
return {
addToCart: item => dispatch(addToCart(item))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Products);
I am not able to load the product list using the component <Product />. Can someone help me why it will not show the list of products (of initialState.products) even though I use mapToStateProps and connect the products with state.products that will contain the initialState of them in the Product component?
Correct me if I'm wrong but you are using initialState.products to initialize the state. Then in your component mapStateToProps you are accessing state.products but your state is already the products itself. So to fix this just do:
function mapStateToProps(state, props) {
return {
products: state
// instead of products: state.products
};
}
So basically what you are doing currently is trying to access initialState.products.products. I hope I explained myself properly.

Resources