After copying array, why can't I edit nested array? - reactjs

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.

Related

How to render the whole value of my array

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

How to conditionally render a list item based on if id matches a websocket object's id

My goal is to use the button on the page to open a websocket connection, subscribe to the ticker feed, then update each list item's price based on the ID of the list item. Currently I have a list that maps through the initial API call's response and saves each object's ID to an array, which in turn is used to build a <li> for each ID. This creates 96 list items. I have also gotten the price to update live via a <p> element in each <li>.
I am having trouble targeting the price for just the matching row ID to the incoming data object's ID so that only that matching row is re-rendered when it gets a match. Below is my code:
ProductRow.js
import React from 'react';
export default function ProductRow(props) {
return <li key={props.id}><p>{ props.id }</p><p>{props.price}</p></li>;
}
WatchList.js
import React, { useState, useEffect, useRef } from "react";
import { Button } from 'react-bootstrap';
import ProductRow from "./ProductRow";
export default function WatchList() {
const [currencies, setcurrencies] = useState([]);
const product_ids = currencies.map((cur) => cur.id);
const [price, setprice] = useState("0.00");
const [isToggle, setToggle] = useState();
const ws = useRef(null);
let first = useRef(false);
const url = "https://api.pro.coinbase.com";
useEffect(() => {
ws.current = new WebSocket("wss://ws-feed.pro.coinbase.com");
let pairs = [];
const apiCall = async () => {
await fetch(url + "/products")
.then((res) => res.json())
.then((data) => (pairs = data));
let filtered = pairs.filter((pair) => {
if (pair.quote_currency === "USD") {
return pair;
}
});
filtered = filtered.sort((a, b) => {
if (a.base_currency < b.base_currency) {
return -1;
}
if (a.base_currency > b.base_currency) {
return 1;
}
return 0;
});
setcurrencies(filtered);
first.current = true;
};
apiCall();
}, []);
useEffect(() => {
ws.current.onmessage = (e) => {
if (!first.current) {
return;
}
let data = JSON.parse(e.data);
if (data.type !== "ticker") {
return;
}
setprice(data.price);
console.log(data.product_id, price);
};
}, [price]);
const handleToggleClick = (e) => {
if (!isToggle) {
let msg = {
type: "subscribe",
product_ids: product_ids,
channels: ["ticker"]
};
let jsonMsg = JSON.stringify(msg);
ws.current.send(jsonMsg);
setToggle(true);
console.log('Toggled On');
}
else {
let unsubMsg = {
type: "unsubscribe",
product_ids: product_ids,
channels: ["ticker"]
};
let unsub = JSON.stringify(unsubMsg);
ws.current.send(unsub);
setToggle(false);
console.log('Toggled Off');
}
};
return (
<div className="container">
<Button onClick={handleToggleClick}><p className="mb-0">Toggle</p></Button>
<ul>
{currencies.map((cur) => {
return <ProductRow id={cur.id} price={price}></ProductRow>
})}
</ul>
</div>
);
}
App.js
import React from "react";
import WatchList from "./components/Watchlist";
import "./scss/App.scss";
export default class App extends React.Component {
render() {
return (
<WatchList></WatchList>
)
}
}
Initialize the price state to be an empty object i.e. {}. We'll refer the price values by the the product_id on getting the response from websocket
// Initialize to empty object
const [price, setprice] = useState({});
...
// Refer and update/add the price by the product_id
useEffect(() => {
ws.current.onmessage = (e) => {
if (!first.current) {
return;
}
let data = JSON.parse(e.data);
if (data.type !== "ticker") {
return;
}
// setprice(data.price)
setprice(prev => ({ ...prev, [data.product_id]: data.price}));
console.log(data.product_id, price);
};
}, [price]);
Render your ProductRows as
<ul>
{currencies.map((cur) => {
return <ProductRow key={cur.id} id={cur.id} price={price[cur.id]}></ProductRow>
})}
</ul>
You don't have to manage anykind of sorting or searching for the relevant prices for products.

How to map an array of maps from firestore in react

I'm trying to navigate an array of orders stored in each "User". I am able to query and find ones that have orders but I'm not able to display them. I keep getting an error "Cannot read property 'map' of null". Where am I going wrong?
The image below shows how all the orders are stored in "order"
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { firestore } from "../../../FireBase/FireBase";
const OrdersAdmin = (props) => {
const [order, setOrder] = useState(null);
useEffect(() => {
const fetchOrder = async () => {
const doc = await firestore.collection("Users");
const snapshot = await doc.where("orders", "!=", []).get();
if (snapshot.empty) {
console.log("No matching documents.");
return <h1>No Orders</h1>;
}
var ans = [];
snapshot.forEach((doc) => {
console.log(doc.id, "=>", doc.data().orders);
setOrder(doc.data().orders)
});
};
fetchOrder();
}, [props]);
return (
<div className="adminOrders">
<h1>orders</h1>
{console.log(order)}
{order.map((orderItem) => (
<div className="singleOrder" key={orderItem.id}>
<p>{orderItem}</p>
</div>
))}
</div>
);
};
export default OrdersAdmin;
The issue is that the initial value of order is null. null does not have Array.prototype.map, therefore you get the error. Try updating your render to use conditional rendering to only attempt Array.prototype.map when order is truthy and an Array:
{order && order.length > 0 && order.map((orderItem) => (
<div className="singleOrder" key={orderItem.id}>
<p>{orderItem}</p>
</div>
))}
Otherwise you can use a better default value of an empty array for order which would have Array.prototype.map available to execute:
const [order, setOrder] = useState([]);
Hopefully that helps!

Modifying object inside array with useContext

I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.

Issue in react component

I have a problem in the following component, it seems that the component doesn't render and I get the following error in console: "Cannot read property 'operationalHours' of null". I don't get why operationalHours it's null.. maybe someone can help me with a posible solution for this issue.
Here is the component:
import React, { useState, useEffect } from 'react';
import Search from 'client/components/ui/Search';
import { performSearchById } from 'client/actions/api/search';
import { get } from 'lodash';
import {
SEARCH_STORE_NOT_CLOSED,
SEARCH_STORE_OPEN_TEXT,
SEARCH_STORE_CLOSED_TEXT
} from 'app/client/constants/values';
import DownArrow from 'components/UI/icons/DownArrow';
import styles from './styles.module.scss';
const StoreDetails = ({ storeInfo }) => {
const [expanded, setIsExpanded] = useState(false);
const [storeData, setStoreData] = useState(null);
useEffect(() => {
async function fetchData() {
const storeId = storeInfo.store_id;
const {
data: {
Location: {
contactDetails: { phone },
operationalHours
}
}
} = await performSearchById(storeId);
setStoreData({ phone, operationalHours });
}
fetchData();
}, [storeInfo.store_id]);
const infoText = expanded ? 'Hide details' : 'View details';
function parseHours(hours) {
const formattedHours = {};
hours.forEach(dayObj => {
const closed = get(dayObj, 'closed', '');
const day = get(dayObj, 'day', '');
if (closed === SEARCH_STORE_NOT_CLOSED) {
const openTime = get(dayObj, 'openTime', '');
const closeTime = get(dayObj, 'closeTime', '');
if (openTime === null || closeTime === null) {
formattedHours[day] = SEARCH_STORE_OPEN_TEXT;
} else {
formattedHours[day] = `${openTime}-${closeTime}`;
}
} else {
formattedHours[day] = SEARCH_STORE_CLOSED_TEXT;
}
});
return formattedHours;
}
const storeHours = storeData.operationalHours
? parseStoreHours(storeData.operationalHours)
: '';
return (
<div className={styles.viewStoreDetails}>
<span
className={expanded ? styles.expanded : undefined}
onClick={() => setIsExpanded(!expanded)}
>
<DownArrow />
</span>
<div>
<span className={styles.viewStoreDetailsLabel}>{infoText}</span>
<div>
{expanded && (
<Search
phoneNumber={storeData.phone}
storeHours={storeHours}
/>
)}
</div>
</div>
</div>
);
};
export default StoreDetails;
Its because you're setting the values of storeData after the component has already rendered the first time. Your default value for storeData is null.
It breaks here: storeData.operationalHours because null isn't an object and therefore cannot have properties to access on it.
You should probably just set your initial state to something more representative of your actual state:
const [storeData, setStoreData] = useState({}); // Or even add keys to the object.
Also read here about the useEffect hook and when it runs. It seems that the underlying issue is misunderstanding when your data will be populated.
You are getting error at this line :
const storeHours = storeData.operationalHours ?
parseStoreHours(storeData.operationalHours): '';
Reason : You initialised storeData as Null and you are trying to access operationalHours key from Null value.
Correct Way is :
Option 1: Initialise storeData as blank object
const [storeData, setStoreData] = useState({});
Option 2:
const storeHours =storeData && storeData.operationalHours ?
parseStoreHours(storeData.operationalHours): '';
It's happen because in 1st moment of your application, storeData is null, and null don't have properties, try add a empty object as first value ({}) or access a value like that:
Correct method:
const object = null;
console.log(object?.myProperty);
// output: undefined
Wrong method:
const object = null;
console.log(object.myProperty);
// Generate a error
The Question Mark(?) is a method to hidden or ignore if the variable are a non-object, to decrease verbosity in the code with logic blocks try and catch, in Correct method code there will be no mistake, but in Wrong method code, there will have a mistake.
Edit 1:
See more here

Resources