I am creating a basic shopping cart app with Reactjs. I created a useContext file to make the states globally available.
Unfortunately, the objects in the useReducer state are not responding to action, except the array of products called 'cart'. The 'amount' and 'total' are not rendering.
Though, the actions can be seen to be updated when I checked the console log. That means I am not returning the right variables.
The action I want to achieve is that when <MdKeyboardArrowUp> is clicked, the 'amount' variable should increase by 1. It increases on console log but not rendered on the page.
The product list:
export default [
{
id: 1,
title: 'Samsung Galaxy S7',
price: 599.99,
img:
'https://res.cloudinary.com/diqqf3eq2/image/upload/v1583368215/phone-2_ohtt5s.png',
amount: 1,
},
useContext file
import React, {useState, useContext, useReducer, useEffect} from 'react';
import cartData from '../component/data'; //this is the source file for the product list//
import customReducer from './reducer'; //the file that handles the useReducer
const Appcontext = React.createContext();
const initialState = {
loading: false,
cart: cartData,
total: 0,
amount: 0,
}
const AppProvider = ({children}) =>{
const [state, dispatch] = useReducer(customReducer, initialState);
const increaseProduct = (id) =>{
dispatch({type: "INCREASE_PRODUCT", payload: id})
}
const decreaseProduct = (id) =>{
dispatch({type: "DECREASE_PRODUCT", payload: id})
}
return(
<Appcontext.Provider value={{...
state,
clearShopCart,
clearShopCart,
removeProduct,
decreaseProduct,
increaseProduct,
}}>
{children}
</Appcontext.Provider>
)
}
export const useGlobalContext = () =>{
return useContext(Appcontext)
}
export {Appcontext, AppProvider}
useReducer file
const customReducer = (state, action) => {
if(action.type === 'CLEAR_SHOPP_CART'){
return{...state, cart: []}
}
if(action.type === 'REMOVE_ITEM'){
const newProducts = state.cart.filter((singleProduct) => singleProduct.id !==
action.payload)
return{...state, cart: newProducts}
}
**if(action.type === "INCREASE_PRODUCT"){
let newValue = state.cart.map((singleProduct) => {
if(singleProduct.id === action.payload){
return {... singleProduct, amount: singleProduct.amount + 1}
}
return singleProduct
});
console.log(newValue)
return {...state, cart: newValue}** //these codes on bold format are the codes that
increases by 1 each time the button is clicked//
}
return state
}
export default customReducer;
The home file where the codes are rendered
import React, { useState, useEffect } from 'react';
import {HiShoppingCart} from 'react-icons/hi';
import {MdKeyboardArrowUp} from 'react-icons/md';
import {RiArrowDownSLine} from 'react-icons/ri';
import { useGlobalContext } from '../component/context';
export default function Home() {
const {cart, amount, total, clearShopCart, removeProduct, decreaseProduct, increaseProduct} =
useGlobalContext();
{cart.map((singleData) => {
const {id, title, price, img} = singleData;
return(
<>
<div key={id} className='product-container'>
<div className='img-container'>
<img src={img} alt={title} />
<div className='product-text-container'>
<h4>{title}</h4>
< h4>${price}</h4>
<h5 className='btn1' onClick={() => removeProduct(id)}>Remove</h5>
</div>
</div>
<div className='item-control'>
<MdKeyboardArrowUp className='iconUp' onClick={() =>
increaseProduct(id)}/> **//when clicked, should inrease 'amount' by
1**//
<p>{amount}</p>
{console.log(amount)}
<RiArrowDownSLine className='iconDown' onClick={() =>
decreaseProduct(id)}/>
</div>
</div>
</>
)
})}
I eventually found the solution. I needed to destructure the 'amount' object coming from the product array. That way, I was able to increase the individual products' amount. It should be like this while destructuring the array:
{cart.map((singleData) => {
const {id, title, price, img, amount} = singleData;
Related
I'm learning react context and while developing a todo application using useContext, I'm facing an issue where on submitting one task, the same task gets added two times to an array. The output component would loop through this array and display the results. While debugging I observed that, although the submit of task add only one entry into the array, not sure why and how, the consumer component gets the array with duplicate entry. Please let me know, what I'm missing.
Here is my code of index file that maintains context
import { createContext, useReducer } from "react";
import ContextReducer, { initialState } from "./ContextReducer";
const taskContext = createContext();
const ContextProvider = (props) => {
const [state, dispatch] = useReducer(ContextReducer, initialState);
const setTaskInput = (taskInput) => {
dispatch({
type: "SET_TASKINPUT",
payload: taskInput,
});
};
const addTask = (task) => {
dispatch({
type: "ADD_TASK",
payload: task,
});
};
const deleteTask = (id) => {
dispatch({
type: "DELETE_TASK",
payload: id,
});
};
const todoContext = {
todo: state.todo,
taskInput: state.taskInput,
setTaskInput,
addTask,
deleteTask,
};
return (
<taskContext.Provider value={todoContext}>
{props.children}
</taskContext.Provider>
);
};
export { taskContext };
export default ContextProvider;
This is the code for reducer
const initialState = {
todo: [],
taskInput: "",
};
const ContextReducer = (state = initialState, action) => {
if (action.type === "SET_TASKINPUT") {
state.taskInput = action.payload;
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
if (action.type === "ADD_TASK") {
state.todo = [...state.todo, action.payload];
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
if (action.type === "DELETE_TASK") {
state.todo = state.todo.filter((todo) => todo.id !== action.payload);
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
return state;
};
export { initialState };
export default ContextReducer;
This is the code of output component or say, consumer component
import React, { Fragment, useContext } from "react";
import { taskContext } from "../../Context";
import styles from "./Content.module.css";
const Output = () => {
const { todo, deleteTask } = useContext(taskContext);
const deleteHandler = (e) => {
deleteTask(+e.target.parentElement.parentElement.id);
};
return (
<Fragment>
{todo.length > 0 && (
<div className={styles.outputDiv}>
<ul>
{todo.map((task) => {
return (
<li key={task.id} id={task.id}>
<div className={styles.row1}>{task.task}</div>
<div className={styles.row2}>
<button className={styles.edit}>Edit</button>
<button className={styles.delete} onClick={deleteHandler}>
Delete
</button>
</div>
</li>
);
})}
</ul>
</div>
)}
</Fragment>
);
};
export default Output;
I am getting an error when I try to filter the results of data I pulled from an API.
The error message comes when I use my searchBar component to filter the redux data immediately when I type anything.
Below is the error message:
"Error: [Immer] An immer producer returned a new value and modified its draft. Either return a new value or modify the draft."
What must I do to filter the data and return the new data?
Below are the components and the Redux TK slice I am using.
Home.js Component
import React, {useState, useEffect} from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { getCountries } from '../redux/search';
// import LazyLoading from 'react-list-lazy-load';
// import { updateText } from '../redux/searchTerm';
import SearchBar from '../components/SearchBar';
import CardComponent from '../components/Card';
import DropdownMenu from '../components/DropdownMenu';
const Home = () => {
const dispatch = useDispatch();
// const [ countries, setCountries ] = useState(null);
const countries = useSelector((state) => state.search);
// const filterCountry = (searchCountry) => {
// countries.list.payload.filter(country => country.name.toLowerCase() == searchCountry.toLowerCase())
// }
useEffect(() => {
dispatch(getCountries());
console.log(countries);
}, [dispatch]);
// console.log(countries.filter(country => country.region.toLowerCase() === 'africa'))
return (
<>
<Container className="home-container">
<Row>
<Col sm={12} md={6} className="left">
<SearchBar />
</Col>
<Col sm={12} md={6} className="right">
<DropdownMenu/>
</Col>
</Row>
<Row className="countryRow">
{ countries.status == 'success'
?<>
{countries.list.map((country) => {
return <CardComponent key={country.name}
title={country.name}
image={country.flags[0]}
population={country.population}
region={country.region}
capital={country.capital}/>
})}
</>
:<div>Loading.....</div>
}
</Row>
</Container>
</>
)
}
export default Home;
SearchBar.js
import React from 'react';
import {useSelector, useDispatch} from 'react-redux';
// import { updateText } from '../redux/searchTerm';
import { searchTerm } from '../redux/search';
const SearchBar = () => {
const query = useSelector((state) => state.searchDefault);
const dispatch = useDispatch();
return (
<>
<form>
<input
className="search"
type="search"
placeholder="Search for a country"
value={query}
onChange={(e) => dispatch(searchTerm(e.target.value))}/>
</form>
</>
)
}
export default SearchBar;
search.js slice
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import axios from 'axios';
export const getCountries = createAsyncThunk(
'searchDefault/getCountries', async () => {
try {
const resp = await axios.get('https://restcountries.com/v2/all');
return await resp.data;
} catch (error) {
console.log(error.message);
}
}
)
const searchSlice = createSlice({
name: 'searchDefault',
initialState: {
list: [],
status: null,
value: ''
},
reducers: {
searchTerm: (state, action) => {
// console.log(state.value);
state.value = action.payload
// console.log(state.value);
state.list.filter( country => country.name == state.value);
return state.list;
// console.log(state.list);
}
},
extraReducers: {
[getCountries.pending]: (state, action) => {
state.status = 'loading'
},
[getCountries.fulfilled]: (state, payload) => {
console.log(payload)
state.list = payload.payload
state.status = 'success'
},
[getCountries.rejected]: (state, action) => {
state.status = 'failed'
}
}
})
export const { searchTerm } = searchSlice.actions;
export default searchSlice.reducer;
According to the docs from redux-toolkit:
Redux Toolkit's createReducer API uses Immer internally automatically. So, it's already safe to "mutate" state inside of any case reducer function that is passed to createReducer:
And Immer docs:
It is also allowed to return arbitrarily other data from the producer function. But only if you didn't modify the draft
In your reducer, you are using immer API to mutate the value (state.value = action.payload) and return the result. However Immer only allows you do one of the 2 things, not both. So either you mutate the state:
searchTerm: (state, action) => {
state.value = action.payload; // notify redux that only the value property is dirty
}
Or replace the new state completely:
searchTerm: (state, action) => {
return { ...state, value: action.payload }; // replace the whole state
// useful when you need to reset all state to the default value
}
Most of the time, you only need to tell redux that a specific property of the slice is changed, and redux will then notify only the components subscribed to that property (via useSelector) to re-render. So remove the return statement in your reducer and your code should be working again:
searchTerm: (state, action) => {
state.value = action.payload;
state.list = state.list.filter( country => country.name == state.value);
// remove the return statement
}
So I have a Context created with reducer. In reducer I have some logic, that in theory should work. I have Show Component that is iterating the data from data.js and has a button.I also have a windows Component that is iterating the data. Anyway the problem is that when I click on button in Show Component it should remove the item/id of data.js in Windows Component and in Show Component, but when I click on it nothing happens. I would be very grateful if someone could help me. Kind regards
App.js
const App =()=>{
const[isShowlOpen, setIsShowOpen]=React.useState(false)
const Show = useRef(null)
function openShow(){
setIsShowOpen(true)
}
function closeShowl(){
setIsShowOpen(false)
}
const handleShow =(e)=>{
if(show.current&& !showl.current.contains(e.target)){
closeShow()
}
}
useEffect(()=>{
document.addEventListener('click',handleShow)
return () =>{
document.removeEventListener('click', handleShow)
}
},[])
return (
<div>
<div ref={show}>
<img className='taskbar__iconsRight' onClick={() =>
setIsShowOpen(!isShowOpen)}
src="https://winaero.com/blog/wp-content/uploads/2017/07/Control-
-icon.png"/>
{isShowOpen ? <Show closeShow={closeShow} />: null}
</div>
)
}
```Context```
import React, { useState, useContext, useReducer, useEffect } from 'react'
import {windowsIcons} from './data'
import reducer from './reducer'
const AppContext = React.createContext()
const initialState = {
icons: windowsIcons
}
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
const remove = (id) => {
dispatch({ type: 'REMOVE', payload: id })
}
return (
<AppContext.Provider
value={{
...state,
remove,
}}
>
{children}
</AppContext.Provider>
)
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
reducer.js
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
return {
...state,
icons: state.icons.filter((windowsIcons) => windowsIcons.id !== action.payload),
}
}
}
export default reducer
``data.js```
export const windowsIcons =[
{
id:15,
url:"something/",
name:"yes",
img:"/images/icons/crud.png",
},
{
id:16,
url:"something/",
name:"nine",
img:"/images/icons/stermm.png",
},
{
id:17,
url:"domething/",
name:"ten",
img:"/images/icons/ll.png",
},
{
id:18,
url:"whatever",
name:"twenty",
img:"/images/icons/icons848.png",
},
{
id:19,
url:"hello",
name:"yeaa",
img:"/images/icons/icons8-96.png",
},
]
``` Show Component```
import React from 'react'
import { useGlobalContext } from '../../context'
import WindowsIcons from '../../WindowsIcons/WindowsIcons'
const Show = () => {
const { remove, } = useGlobalContext()
return (
<div className='control'>
{windowsIcons.map((unin)=>{
const { name, img, id} = unin
return (
<li className='control' key ={id}>
<div className='img__text'>
<img className='control__Img' src={img} />
<h4 className='control__name'>{name}</h4>
</div>
<button className='unin__button' onClick={() => remove(id)} >remove</button>
</li> )
</div>
)
}
export default Show
import React from 'react'
import {windowsIcons} from "../data"
import './WindowsIcons.css'
const WindowsIcons = ({id, url, img, name}) => {
return (
<>
{windowsIcons.map((icons)=>{
const {id, name , img ,url} =icons
return(
<div className='windows__icon' >
<li className='windows__list' key={id}>
<a href={url}>
<img className='windows__image' src={img}/>
<h4 className='windows__text'>{name}</h4>
</a>
</li>
</div>
)
})}
</>
)
}
Issue
In the reducer you are setting the initial state to your data list.
This is all correct.
However, then in your Show component you are directly importing windowsIcons and looping over it to render. So you are no longer looping over the state the reducer is handling. If the state changes, you won't see it.
Solution
In your Show component instead loop over the state that you have in the reducer:
const { remove, icons } = useGlobalContext()
{icons.map((unin) => {
// Render stuff
}
Now if you click remove it will modify the internal state and the icons variable will get updated.
Codesandbox working example
I'm working on an e-commerce project using React Redux, and can't seem to get my head around this error that's been bugging me. I'm trying to add the clicked item (along with its props like name, imageUrl, etc.) to the cartItems array through the cart reducer. Created a function to do the adding in utils and trying to import and use that in the reducer (function adds a new prop to the item called quantity to count the frequency of the item). But for some reason, the function is creating some new element with just 1 prop, quantity every time, as opposed to adding the clicked element with the added prop.
My actions component code -
import CartActionTypes from './cart.types';
export const toggleCartHidden = () => ({
type: CartActionTypes.TOGGLE_CART_HIDDEN
});
export const addItem = item => ({
type: CartActionTypes.ADD_ITEM,
action: item
});
cart reducer -
import CartActionTypes from './cart.types';
import { addItemToCart } from './cart.utils';
const INITIAL_STATE = {
hidden: true,
cartItems: []
};
const cartReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case CartActionTypes.TOGGLE_CART_HIDDEN:
return {
...state,
hidden: !state.hidden
};
case CartActionTypes.ADD_ITEM:
return {
...state,
cartItems: addItemToCart(state.cartItems, action.payload)
};
default:
return state;
}
};
export default cartReducer;
utils -
export const addItemToCart = (cartItems, cartItemToAdd) => {
const existingCartItem = cartItems.find(
cartItem => cartItem.id === cartItemToAdd.id
);
if (existingCartItem) {
return cartItems.map(cartItem =>
cartItem.id === cartItemToAdd.id
? { ...cartItem, quantity: cartItem.quantity + 1 }
: cartItem
)
}
return [...cartItems, { ...cartItemToAdd, quantity: 1 }];
};
the collection item component below, which displays the item that should be added (this component is called into the display component which maps the item data to the item component to display all the items). The item clicked should get added with the quantity prop created for the same item through the reducer.
import React from 'react';
import { connect } from 'react-redux';
import CustomButton from '../custom-button/custom-button.component';
import { addItem } from '../../Redux/cart/cart.actions';
import './collection-item.styles.css';
const CollectionItem = ({ item, addItem }) => {
const { name, price, imageUrl } = item;
return (
<div className='collection-item'>
<div
className='image'
style={{
backgroundImage: `url(${imageUrl})`
}}
/>
<div className='collection-footer'>
<span className='name'>{name}</span>
<span className='price'>{price}</span>
</div>
<CustomButton onClick={() => addItem(item)} inverted>
Add to cart
</CustomButton>
</div>
);
};
const mapDispatchToProps = dispatch => ({
addItem: item => dispatch(addItem(item))
});
export default connect(
null,
mapDispatchToProps
)(CollectionItem);
Collection preview (uses CollectionItem) -
import React from 'react';
import CollectionItem from '../collection-item/collection-item.component';
import './collection-preview.styles.css';
const CollectionPreview = ({ title, items }) => (
<div className='collection-preview'>
<h1 className='title'>{title.toUpperCase()}</h1>
<div className='preview'>
{items
.filter((item, idx) => idx < 4)
.map(item => (
<CollectionItem key={item.id} item={item} />
))}
</div>
</div>
);
export default CollectionPreview;
The error screenshot -
The right (ADD_ITEM) action is getting triggered and it is capturing that clicked element, but its somehow not getting added to the cartItems array. Any help would be appreciated.
I am having a problem where i'm trying to render pass an array's data to a card component but it doesn't appear on the page, the card component renders normally on its own:
import React, { Component } from 'react'
import { Container, Grid, Card, Segment, Header } from 'semantic-ui-react';
import ArticleCard from './ArticleCard';
export default class NewsGrid extends Component {
render() {
const {News} = this.props
console.log(News)
return (
<div>
<Container style={{marginTop: '7%'}}>
<Grid stackable divided >
<Grid.Column style={{width: '66.66%'}}>
<Card.Group>
{
News.map(({id, ...otherArticleProps}) => (
<ArticleCard key={id} {...otherArticleProps} />
))
}
</Card.Group>
</Grid.Column>
</Grid>
</Container>
</div>
)
}
}
console.log() shows that the data is actually there
and i'm passing the array as props from the parent page component, the data is delivered through the flamelink API in useEffect as shown bellow:
import React, { useEffect, useState } from 'react';
import NewsGrid from '../components/NewsGrid';
import BusinessContainer from '../components/BusinessContainer';
import PolitiqueContainer from './../components/PolitiqueContainer';
import app from '../Firebase';
import { withRouter } from 'react-router-dom';
const Homepage = () => {
const [News] = useState([])
const [Business] = useState([])
const [Politique] = useState([])
const [Opinion] = useState([])
const [Blog] = useState([])
useEffect(() => {
app.content.get({schemaKey: 'articles',
fields: ['title', 'author', 'date', 'thumbnail', 'slug', 'summary', 'category', 'id'],
orderBy:{field: 'date',
order: 'desc'},
})
.then(articles => {
for(var propName in articles) {
const propValue = articles[propName]
switch(propValue.category) {
default :
break;
case 'News':
News.push(propValue)
break;
case 'Business':
Business.push(propValue)
break;
case 'Politique':
Politique.push(propValue)
break;
case 'Opinion':
Opinion.push(propValue)
break;
case 'Blog':
Blog.push(propValue)
break;
}
}
})
})
return (
<div >
<NewsGrid News={News} Opinion={Opinion} Blog={Blog} />
<BusinessContainer content={Business} />
<PolitiqueContainer content={Politique} />
</div>
);
};
export default withRouter(Homepage);
I am using firebase as a backend combined with Flamelink for the CMS.
Thanks in advance.
When you use const [News] = React.useState([]), any changes you make to News will not be reflected in your React application, since mutating News will not update the state of your component. If you want to update the state of News, you need to use the state dispatch function provided by React.useState. Try this instead:
const [News, updateNews] = React.useState([])
// We can't use News.push('Some news'), but we can update News this way:
updateNews([...News, 'Some news'])
import React, { useEffect, useState } from 'react';
import NewsGrid from '../components/NewsGrid';
import BusinessContainer from '../components/BusinessContainer';
import PolitiqueContainer from './../components/PolitiqueContainer';
import app from '../Firebase';
import { withRouter } from 'react-router-dom';
const Homepage = () => {
const [news, setNews] = useState([]);
const [business, setBusiness] = useState([]);
const [politique, setPolitique] = useState([]);
const [opinion, setOpinion] = useState([]);
const [blog, setBlog] = useState([]);
useEffect(() => {
app.content
.get({
schemaKey: 'articles',
fields: ['title', 'author', 'date', 'thumbnail', 'slug', 'summary', 'category', 'id'],
orderBy: { field: 'date', order: 'desc' },
})
.then(articles => {
for (let propName in articles) {
const propValue = articles[propName];
switch (propValue.category) {
case 'News':
setNews(propValue);
break;
case 'Business':
setBusiness(propValue);
break;
case 'Politique':
setPolitique(propValue);
break;
case 'Opinion':
setOpinion(propValue);
break;
case 'Blog':
setBlog(propValue);
break;
default:
break;
}
}
});
}, []);
return (
<div>
<NewsGrid News={news} Opinion={opinion} Blog={blog} />
<BusinessContainer content={business} />
<PolitiqueContainer content={politique} />
</div>
);
};
export default withRouter(Homepage);
I would suggest using camel case variables:
const [stateVariable, setStateVariable] = useState();
(https://reactjs.org/docs/hooks-state.html) and to add the [] to the useEffect array of dependencies. This will ensure that the useEffect will trigger only a single time. Hope that this helps.