Iam still a bit new to react and how it works. I'm trying to create a shopping list from a list of recipe that are objects who contain an array of ingredient but react only displays the 1st element and displays the other elements after each click
here is my code :
import { useState } from 'react';
import { MealContext } from '../../../Context/MealContext';
import GroceryListComponent from './GroceryListComponent';
const GrocerysListContainer = () => {
const [selectedMeals,setMeals] = useContext(MealContext);
const [groceryList, setGroceryList]= useState([]);
const [showList,setShowList] = useState(false);
const handleClick = ()=>{
setShowList(true);
selectedMeals.forEach(meal => {
meal.ingredient.forEach((dish)=>{
if(!groceryList.find(({name})=>name === dish.toLowerCase())){
setGroceryList([...groceryList, {name : dish.toLowerCase(), qty : 1}]);
console.log('add')
}else{
console.log('remove')
return;
}
})
});
}
return (
<div className="groceryList">
<button className="btn btn-submit" onClick={()=>handleClick()}>Creer liste de course</button>
{showList && <ul>
{ groceryList.map((grocery,index)=>{
return(
<GroceryListComponent
key={index}
grocery={grocery}
/>
)
})
}
</ul>}
</div>
);
};
export default GrocerysListContainer;
Most likey groceryList is not getting updated in function, so it will help to add the new elements to a array and then finally add them
Quick fix
setGroceryList(list => [...list, {name : dish.toLowerCase(), qty : 1}]);
To move towards a improved solution, the quantity of existing item also needs to be udpated
const handleClick = () => {
// can include try catch
setShowList(true);
let newList = []
for (const meal of selectedMeals) {
// include check with ?. and ??
const ingredient = meal.ingredient;
// can add another loop here
const dish = ingredient.find(dish => !groceryList.find(({ name }) => name === dish.toLowerCase()))
if (!dish) {
newList.push({ name : dish.toLowerCase(), qty : 1})
} else {
// depends on what you want to achieve
// increase qty or something
}
}
setGroceryList([ ...groceryList, ...newList ])
}
Hope it helps you in some way
Cheers
I'm trying to edit an array by removing a specific date. I'm using React18 and Redux Toolkit holds the original array, but for some reason after copying it, I cannot edit the array. Here is the current error message;
"Uncaught TypeError: Cannot assign to read only property 'dates' of object '#'"
What is wrong with my approach?
import { useDispatch, useSelector } from "react-redux";
import { setCurrentMonthBookings } from "./location";
const Component = () => {
const { booking, currentMonthBookings } = useSelector(state => state.calendar);
const handleDelete = () => {
let reservations = currentMonthBookings.slice();
const bookingIndex = reservations.findIndex(
(curBooking) =>
curBooking.date === booking.date && curBooking.id === booking.id,
);
const newDates = reservations[bookingIndex].dates.filter(
(date) => date !== booking.date,
);
reservations.splice(bookingIndex, 1);
reservations.forEach((reservation) => {
if (reservation.id === booking.id) {
reservation.dates = newDates; //error happens here...
}
});
dispatch(setCurrentMonthBookings(reservations));
}
return (
<div>
<button onClick={handleDelete}>Delete It</button>
</div>
);
}
export default Component;
What the booking object looks like...
{
date: "2022-05-03",
dates: (2) ['2022-05-03', '2022-05-04'],
guestId: "1938479385798579",
id: "9879287498765"
}
The currentMonthBookings array is a series of booking objects.
Thank you for your replies.
I am currently developing a front end application using react-redux. But I am very new to this language.
So basically I have the following UI
What I am trying to achieve is whenever users increase or decrease the option, it will store to the store procedure, and finally make an API call to backend and calculate pricing.
Before API call, my idea is I will let users to increase/decrease the option and finally when the user is done, i will take that array of object and submit to the api endpoint.
Unfortunately, It seems like the following scenario is failed.
I increase option 1, it will save to the state as an array of object
first time with quantity and optionId [OK]
After that, I will increase the option 2, since it is the new option,
I will push the object to the existing array. [OK]
When I try to increase option 1 again, it has to check whether option
1 is already inside the array, if there is option 1, it will just
increase that option quantity. but my code does not behave that way. [FAILED]
below is my Component
import React, {Component} from 'react';
import {handleIncreaseOption} from '../actions/option';
import {Button, Card, Col, Row, Statistic} from "antd";
import {MinusOutlined, PlusOutlined} from '#ant-design/icons';
import {connect} from 'react-redux';
const {Meta} = Card;
class FlavourCard extends Component {
state = {
quantity: 0,
optionId: this.props.optionId
}
increase = () => {
let count = this.state.quantity + 1;
this.setState({
quantity: count,
optionId: this.props.optionId
}, function(){
console.log('this state before going in', this.state);
this.props.dispatch(handleIncreaseOption(this.state));
});
}
decline = () => {
let count = this.state.count - 1;
if (count < 0) {
count = 0;
}
this.setState({count: count});
console.log(this.state);
}
render() {
const {flavourImg, itemTitle} = this.props;
return (
<Card
hoverable
cover={<img alt="example" className="flavour-img" src={flavourImg}/>}
>
<Meta
title={itemTitle}
style={{textAlign: 'center'}}
description={
<Row justify="start" gutter={12}>
<Col span={10} style={{textAlign: 'right', paddingTop: '6px'}}>
<Button onClick={this.decline} size="small">
<MinusOutlined/>
</Button>
</Col>
<Col span={4}>
<Statistic value={this.state.quantity} style={{fontSize: '10px'}}/>
</Col>
<Col span={10} style={{textAlign: 'left', paddingTop: '6px'}}>
<Button onClick={this.increase} size="small">
<PlusOutlined/>
</Button>
</Col>
</Row>
}
/>
</Card>
)
}
}
function mapStateToProps(state) {
return{
loadingBar: state.loadingBar
}
}
export default connect(mapStateToProps) (FlavourCard)
This is my action class
export const RETRIEVE_OPTIONS = 'RETRIEVE_OPTIONS';
export const INCREASE_OPTIONS = 'INCREASE_OPTIONS';
export function receiveOptions( option ) {
return {
type: RETRIEVE_OPTIONS,
option
}
}
export function handleIncreaseOption ( option ) {
return {
type: INCREASE_OPTIONS,
option
}
}
This is my reducer
import {RETRIEVE_OPTIONS, INCREASE_OPTIONS} from "../actions/option";
export default function option ( state = null , action )
{
switch (action.type) {
case RETRIEVE_OPTIONS:
return {
...state,
...action
}
case INCREASE_OPTIONS:
if ( !state.hasOwnProperty('addOption') ) {
return {
...state,
addOption: [
{
quantity: action.option.quantity,
optionId: action.option.optionId
}
]
}
}
state.addOption.map((opt) => {
if(opt.optionId === action.option.optionId) {
opt.quantity = action.option.quantity;
}else {
let originalAddOption = state.addOption;
originalAddOption.push({
quantity: action.option.quantity,
optionId: action.option.optionId
})
}
return {
...state,
...action
}
})
default:
return state
}
}
I believe that my "INCREASE_OPTIONS" reducer is something wrong, because, the correct logic should be when there is a new optionId, it will add in as a new object, and if the optionId is existing one, it will just increase the entity. For my current code, whenever I make a second option to increase, it will just add in a new object with new quantity value. I have attached the console result below
How can I achieve when there is existing option, just increase/decrease the quantity and if option is newly added, make a new object and push to the array? Thanks in advance
There are a couple problems in the reducer.
The first issue is that you are trying to update the state object directly. This will not work, you have to set state to a new object.
The second issue is how you are using the map function. It looks like you are using it to update a value if it exists, or add a new entry if it does not. You might have to separate that out and first check if it exists, if so do an update, if not add a new element. Then for each opt in the array, you return an object containing the entire state and action, which I don't think is your intention.
Try out something like this in the reducer:
case INCREASE_OPTIONS: {
if ( !state.hasOwnProperty('addOption') ) {
return {
...state,
addOption: [
{
quantity: action.option.quantity,
optionId: action.option.optionId
}
]
}
}
let updated = false;
// For every element, check if we find the id to modify
// Map returns an array. Does not modify in place.
let addOptCopy = state.addOption.map((opt) => {
if(opt.optionId === action.option.optionId) {
opt.quantity = action.option.quantity;
updated = true;
}
return opt;
});
// If nothing was updated, push new element
if(!updated){
addOptCopy.push({
quantity: action.option.quantity,
optionId: action.option.optionId
})
}
// return the new state
return {
...state,
addOption: [...addOptCopy]
}
}
As one of the comments on your post suggested, it may be an over complication to be keeping two states, using the components state plus redux state and keeping them in sync. You can do the increase and decrease within the reducer, and get the state from props in the components by linking it in mapStateToProps.
Lastly, there seems to be a typo in the decrease function, you are setting count in state instead of quantity.
Here is a functional example, you only need to pass id to the increaseOption action creator:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
data: [
{
id: 1,
},
{
id: 2,
},
],
};
//action types
const INCREASE_OPTIONS = 'INCREASE_OPTIONS';
//action creators
const increaseOption = (id) => ({
type: INCREASE_OPTIONS,
payload: id,
});
const reducer = (state, { type, payload }) => {
if (type === INCREASE_OPTIONS) {
const addOption = state.addOption || [];
const exist = addOption.some(
({ optionId }) => optionId === payload
);
return {
...state,
addOption: exist
? addOption.map((option) =>
option.optionId === payload
? { ...option, quantity: option.quantity + 1 }
: option
)
: addOption.concat({
optionId: payload,
quantity: 1,
}),
};
}
return state;
};
//selectors
const selectData = (state) => state.data;
const selectOption = (state) => state.addOption || [];
const createSelectOption = (id) =>
createSelector([selectOption], (options) => {
const option = options.find(
({ optionId }) => optionId === id
);
return option ? option.quantity : 0;
});
const createSelectItem = (itemId) =>
createSelector([selectData], (data) =>
data.find(({ id }) => id === itemId)
);
const createSelectCardProp = (id) =>
createSelector(
[createSelectOption(id), createSelectItem(id)],
(option, item) => ({ option, item })
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (n) => (a) => n(a))
)
);
const FlavourCard = React.memo(function FlavourCard({
id,
}) {
const dispatch = useDispatch();
const selectProps = React.useMemo(
() => createSelectCardProp(id),
[id]
);
const props = useSelector(selectProps);
return (
<button onClick={() => dispatch(increaseOption(id))}>
id: {props.item.id} count:{props.option}
</button>
);
});
const App = () => {
const data = useSelector(selectData);
return (
<ul>
{data.map(({ id }) => (
<FlavourCard key={id} id={id} />
))}
</ul>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
The following selectManageAdvancedUserFilters selector cause to render my component twice, but without that selector it renders only one time
export const selectManageAdvancedUserFilters = typeCode => {
return createSelector([selectUserFilters(typeCode)], userFilters => {
const manageAdvancedFilters = userFilters.map((filter, index) => {
return {
index: index + 1,
label: filter.name,
value: filter.name,
id: filter.id
};
});
return manageAdvancedFilters;
});
};
export const selectUserFilters = typeCode => {
return createSelector([selectAllUserFilters], allUserFilters =>
allUserFilters.filter(allUserFilter => allUserFilter.type === typeCode)
);
};
export const selectAllUserFilters = createSelector(
[selectControls],
controls => {
return controls && controls.advancedFilters ? controls.advancedFilters : [];
}
);
export const selectControls = state => {
return state.controls.data;
};
here is the usage of selector
const unallocatedFilters = useSelector(
selectDropdownSuggestionFilters('AF1')
)
What is the reason cause to rerender with this selector?
I think the re-render is caused by the implementation details of the dynamic parameter, typeCode.
See the link below:
https://github.com/reduxjs/reselect/issues/392
I am receiving some data as props and on click I am trying to display next items from array. In render I'm calling {this.dropdown()} which triggers folowing and display data succedsfully:
dropdown = () => {
var dropdown = undefined
if(this.props.catList){
const cat = this.props.catList
const list = JSON.stringify(this.props.catList)
if(list.length > 0){
dropdown = <div><p onClick={() =>{this.subCat()}}>{cat.title}</p>{this.state.firstSubCat}</div>
}
}
return dropdown
}
Next, when I click on item, sub categories is displayed with no issues and generates place where to display state for next function {this.state['sub'+cat.id]} :
subCat = () => {
let subCat = []
this.props.catList.children.map(cat => {
subCat.push(<div key={cat.id}><p key={cat.id} onClick={() =>{this.searchSubCat(cat.id)}}>{cat.title}</p>{this.state['sub'+cat.id]}</div>)
})
this.setState({firstSubCat: subCat})
}
Next two function is for loop through rest of array to display next items on click. (Please note that I did not use it from beginning because first line of data is not objects but contains 'children' as array so now i can use these two functions):
find = (array, id) => {
// Loop the entries at this level
for (const entry of array) {
// If we found it, return it
if (entry.id === id) {
return entry;
}
// If not but there's a type array, recursively search it
if (Array.isArray(entry.type)) {
const found = find(entry.type, id);
if (found) {
// Recursive search found it, return it
return found;
}
}
}
return undefined
}
searchSubCat = (id) => {
let subCat = []
const children = this.find(this.props.catList.children, id)
children.children.map(cat => {
subCat.push(<div key={cat.id}><p key={cat.id} onClick={() =>{this.searchSubCat(cat.id)}}>{cat.title}</p>{this.state['sub'+cat.id]}</div>)
})
this.setState({['sub' + id]: subCat})
}
So far there is no errors poping up but in generated place state is not being displayed. When generating place ( with this: {this.state['sub'+cat.id]} ) where to display state I pass its id to next step to set state with same id so state should be displayed there but nothing. If anybody can see where is issue could please respond could help me here out would be great ! Thanks.
Full code for component as requested in comment:
import React, {Component} from 'react';
class SearchResult extends Component {
constructor( props ){
super( props );
this.state = {
}
}
find = (array, id) => {
// Loop the entries at this level
for (const entry of array) {
// If we found it, return it
if (entry.id === id) {
return entry;
}
// If not but there's a type array, recursively search it
if (Array.isArray(entry.type)) {
const found = find(entry.type, id);
if (found) {
// Recursive search found it, return it
return found;
}
}
}
return undefined
}
searchSubCat = (id) => {
let subCat = []
const subId = 'sub' + id
console.log(subId)
const children = this.find(this.props.catList.children, id)
children.children.map(cat => {
subCat.push(<div key={cat.id}><p key={cat.id} onClick={() =>{this.searchSubCat(cat.id)}}>{cat.title}</p>{this.state['sub'+cat.id]}</div>)
})
this.setState({['sub' + id]: subCat})
}
subCat = () => {
let subCat = []
this.props.catList.children.map(cat => {
subCat.push(<div key={cat.id}><p key={cat.id} onClick={() =>{this.searchSubCat(cat.id)}}>{cat.title}</p>{this.state['sub'+cat.id]}</div>)
console.log('sub--'+cat.id)
})
this.setState({firstSubCat: subCat})
}
dropdown = () => {
var dropdown = undefined
if(this.props.catList){
const cat = this.props.catList
const list = JSON.stringify(this.props.catList)
if(list.length > 0){
dropdown = <div><p onClick={() =>{this.subCat()}}>{cat.title}</p>{this.state.firstSubCat}</div>
}
}
return dropdown
}
render() {
return (
<div>
{this.dropdown()}
</div>
)
}
}
export default SearchResult;
UPDATED:
I receive array from server with Redux which I send to my first component where I use map() method to find 1st level of array and send it to component with its childrens as props(catList). Cant really copy and paste catList prop value here so here is array, how i pass it and IMG of console.log(this.props.catList) :
Array:
[{"id":1,"title":"Electronics","path":"Electronics","children":[{"id":2,"title":"Accessories","children":[{"id":6,"title":"Portable Power Banks","children":[]},{"id":7,"title":"Charging Cables","children":[]},{"id":9,"title":"Batteries","children":[{"id":10,"title":"Disposable","children":[]},{"id":19,"title":"Rechargable","children":[]}]}]},{"id":3,"title":"Computers","children":[{"id":4,"title":"Components","children":[{"id":5,"title":"Laptops","children":[]}]}]}]},{"id":2,"title":"Accessories","path":"Electronics->Accessories","children":[{"id":6,"title":"Portable Power Banks","children":[]},{"id":7,"title":"Charging Cables","children":[]},{"id":9,"title":"Batteries","children":[{"id":10,"title":"Disposable","children":[]},{"id":19,"title":"Rechargable","children":[]}]}]},{"id":6,"title":"Portable Power Banks","path":"Electronics->Accessories->Portable Power Banks","children":null},{"id":7,"title":"Charging Cables","path":"Electronics->Accessories->Charging Cables","children":null},{"id":9,"title":"Batteries","path":"Electronics->Accessories->Batteries","children":[{"id":10,"title":"Disposable","children":[]},{"id":19,"title":"Rechargable","children":[]}]},{"id":10,"title":"Disposable","path":"Electronics->Accessories->Batteries->Disposable","children":null},{"id":19,"title":"Rechargable","path":"Electronics->Accessories->Batteries->Rechargable","children":null},{"id":3,"title":"Computers","path":"Electronics->Accessories->Computers","children":[{"id":4,"title":"Components","children":[{"id":5,"title":"Laptops","children":[]}]}]},{"id":4,"title":"Components","path":"Electronics->Accessories->Computers->Components","children":[{"id":5,"title":"Laptops","children":[]}]},{"id":5,"title":"Laptops","path":"Electronics->Accessories->Computers->Components->Laptops","children":null},{"id":11,"title":"Cars","path":"Cars","children":[{"id":12,"title":"Electronics","children":[{"id":13,"title":"Accessories","children":[{"id":14,"title":"Chargers","children":[]}]}]}]},{"id":12,"title":"Electronics","path":"Cars->Electronics","children":[{"id":13,"title":"Accessories","children":[{"id":14,"title":"Chargers","children":[]}]}]},{"id":13,"title":"Accessories","path":"Cars->Electronics->Accessories","children":[{"id":14,"title":"Chargers","children":[]}]},{"id":14,"title":"Chargers","path":"Cars->Electronics->Accessories->Chargers","children":null},{"id":15,"title":"DIY","path":"DIY","children":[{"id":16,"title":"Power Tools","children":[{"id":17,"title":"Accessories","children":[{"id":18,"title":"Batteries","children":[]}]}]}]},{"id":16,"title":"Power Tools","path":"DIY->Power Tools","children":[{"id":17,"title":"Accessories","children":[{"id":18,"title":"Batteries","children":[]}]}]},{"id":17,"title":"Accessories","path":"DIY->Power Tools->Accessories","children":[{"id":18,"title":"Batteries","children":[]}]},{"id":18,"title":"Batteries","path":"DIY->Power Tools->Accessories->Batteries","children":null}]
and here i use map() method from where prop catList is passed to component:
this.props.searchRes.map(cat => {
if(!cat.path.includes('->')){
categories.push(<SearchResult filtered={false} title={cat.title} id={cat.id} catList={cat} key={cat.id}/>)
}
})