HTMLAudioElement pause not triggering in ReactJS - javascript

I am able to fetch and display audio information, and also to trigger playback but could not pause the media item by running the pause function. At least, it is not receiving an event in "onpause" and the audio keeps running.
Using ref should work as expected. The tested audio is served by https://arweave.net/, if that makes a difference.
Can you spot the issue?
export const Player = (): JSX.Element => {
const [playingAudio, setPlayingAudio] = useState<boolean>(false);
const { fetchMetadata, metadata } = useContract();
const theme: Theme = useTheme();
const audio = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
const updateMetadata = async () => await fetchMetadata();
updateMetadata();
if (metadata) {
audio.current = new Audio(metadata.animation_url);
if (audio.current) {
audio.current.onpause = () => setPlayingAudio(false);
audio.current.onended = () => setPlayingAudio(false);
audio.current.onplay = () => setPlayingAudio(true);
}
}
}, [fetchMetadata, metadata]);
const playAudio = async (): Promise<void> => await audio?.current?.play();
const pauseAudio = (): void => audio?.current?.pause();
return (
<Card sx={{ width: '100%', maxWidth: '600px', display: 'flex', justifyContent: 'space-between', marginY: 3 }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<CardContent sx={{ flex: '1 0 auto' }}>
<Typography component="div" variant="h5">
{metadata?.name}
</Typography>
<Typography variant="subtitle1" color="text.secondary" component="div">
{metadata?.description}
</Typography>
</CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', pl: 1, pb: 1 }}>
<IconButton aria-label="previous">
{theme.direction === 'rtl' ? <SkipNextIcon /> : <SkipPreviousIcon />}
</IconButton>
{playingAudio ? (
<IconButton aria-label="play/pause" onClick={pauseAudio}>
<PauseCircle sx={{ height: 38, width: 38 }} />
</IconButton>
) : (
<IconButton aria-label="play/pause" onClick={playAudio}>
<PlayArrowIcon sx={{ height: 38, width: 38 }} />
</IconButton>
)}
<IconButton aria-label="next">
{theme.direction === 'rtl' ? <SkipPreviousIcon /> : <SkipNextIcon />}
</IconButton>
</Box>
</Box>
<CardMedia
component="img"
sx={{ width: 200 }}
image={metadata?.image}
alt={metadata?.name}
/>
</Card>
);
};

As #morganney hinted, I should check the presence of the metadata, before updating it again (as it was in a loop), so now no unnecessary re-rendering is happening, and I can pause the audio.
useEffect(() => {
const updateMetadata = async () => await fetchMetadata();
if (!metadata) updateMetadata();
if (metadata) {
audio.current = new Audio(metadata.animation_url);
if (audio.current) {
audio.current.onpause = () => setPlayingAudio(false);
audio.current.onended = () => setPlayingAudio(false);
audio.current.onplay = () => setPlayingAudio(true);
}
}
}, [fetchMetadata, metadata]);
(the rest of the code stays the same)

Related

Display data based on response code from server after loader is loader is completed inside JSX?

I'm using react and redux-toolkit to call the backend API. I have create initial state like
const initialState = {
sessionInfo: {},
loading: false
};
export const validateSession = createAsyncThunk(
'merchant/validate',
async (params, {dispatch}) => {
const data = {
type : "",
source : "",
}
const res = await SessionValidate.validateSession(data)
if(res.status === 200) {
return res.data
}
}
)
const sessionValidation = createSlice({
name:"sessionValidation",
initialState,
extraReducers: {
[validateSession.fulfilled]: (state, { payload }) => {
state.sessionInfo = payload
state.loading = false
},
[validateSession.pending]: (state, { paylaod }) => {
state.loading = true
},
[validateSession.rejected]: (state, { paylaod }) => {
state.loading = false
}
}
})
Now, I fetch the store data from one of the react component to display the loader and data.
I already written working JSX code for my desired output.
NOW THE PROBLEM STATEMENT
Redux-tookit's extrareducers have this lifecycle of fulfilled, reject , pending. Now, on pending state the loader state becomes TRUE. That point the loader component needs to fireup inside JSX.
Later, request got fulfilled and response is saved inside sessionInfo object and loader becomes false.
Now my designed output should be.
Show loader on api startup and once api request is completed, remove loader from jsx and check the code inside response object and display content accordingly.
Everthing works fine, can i refactor the below JSX to properly maintain code.
export default function HomePage() {
const dispatch = useDispatch();
const { sessionInfo, loading } = useSelector(state => state.session)
const nextPath = (path) => {
this.props.history.push(path);
};
React.useEffect(() => {
dispatch(validateSession())
}, [])
const sessionValidation = (
<>
{ loading ?
<Box textAlign="center" sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<CircularProgress size={25} sx={{ mr: 2 }} />
<Typography variant="h4">Validating session.. please wait</Typography>
</Box> :
<Box textAlign="center" sx={{ justifyContent: 'center', alignItems: 'center' }}>
{ sessionInfo && sessionInfo.code === 403 ?
<>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<ErrorIcon fontSize="large" sx={{ mr: 1 }} color="error" />
<Typography variant="h4">{"SESSION EXPIRED" }</Typography>
</div>
<Typography sx={{ textAlign: "center", mt: 2 }}>
<Button component={Link} to = "/home" variant="contained" color="primary">
Go back
</Button>
</Typography>
</>
:
<>
<div>
<CheckCircleIcon sx={{ mr: 1 }} color="success" />
<Typography>{"SESSION VALIDATION SUCCEEDED" }</Typography>
</div>
<Typography sx={{ textAlign: "center", mt: 2 }}>
<Link to = "/home">Get Started</Link>
<Button component={Link} to = "/home" variant="contained" color="primary">
Get Started
</Button>
</Typography>
</>
}
</Box>
}
</>
)
return (
<Container maxWidth="sm" sx={{ mt: 4 }}>
<Paper elevation={1} sx={{ py: 4, borderTop: "4px solid #F76E40" }}>
{ sessionValidation }
</Paper>
</Container>
);
}

Uncaught TypeError: setStatusFilters is not a function in react

So In my react application I have filtered option dialog. When a user select filter option and there is no related data, the dialog prompts No objects found based on your filter, try changing filter criteria and below this text I have a button called back to reset the filters. To do so I have defined a function resetAllFilters inside this function I called setStatusFilters.
But when I call resetAllFilters function I got Uncaught TypeError: setStatusFilters is not a function at resetAllFilters
Here is my code.
import { Search } from "#mui/icons-material";
import { Divider, Grid, List, Stack, Typography } from "#mui/material";
import React, { useEffect, useState, useMemo } from "react";
import TextInput from "../../../components/input/TextInput";
import Loading from "../../../components/loading/Loading";
import { filterObjects, filterSearch } from "../../../data/helpers/Helpers";
import TrackingFilterContainer from "../containers/TrackingFilterContainer";
import ObjectListItem from "./ObjectListItem";
import {
makeDefaultFilterState,
makeFilterOptionsObj,
} from "../../../data/helpers/Helpers";
import Button from "../../../components/button/Button";
const classes = {
Root: {
height: "100vh",
},
SearchInput: (theme) => ({
mt: "5%",
// ml: "5%",
p: 0.7,
borderRadius: "5px",
width: "100%",
bgcolor: "white",
"& input": {
overflow: "hidden",
textOverflow: "ellipsis",
},
"& svg": {
mr: "2.5%",
},
[theme.breakpoints.down("md")]: {
mt: 0,
pl: 1.5,
bgcolor: "background.primary",
},
}),
SearchLogo: { color: "misc.hint" },
Divider: (theme) => ({
mt: "2.5%",
width: "100%",
mb: 3,
[theme.breakpoints.down("md")]: {
display: "none",
},
}),
FilterWrapper: {
// mt: "3%",
px: "2%",
pt: "5%",
pb: "3%",
columnGap: "3%",
},
ListWrapper: {
// mt: "1.5%",
height: "100%",
overflow: "auto",
"&::-webkit-scrollbar": {
width: "3px",
},
"&::-webkit-scrollbar-thumb": {
borderRadius: "8px",
backgroundColor: "misc.hint",
},
},
};
function ObjectList({
search,
setSearch,
trackingObjects,
loading,
error,
selectedObj,
setSelectedObj,
statusFilters,
setStatusCheckboxFilters,
setStatusFilters,
}) {
const handleClick = React.useCallback((obj) => {
setSelectedObj(obj);
}, []);
console.log(statusFilters, "object");
const resultObjs = filterSearch(
filterObjects(trackingObjects, statusFilters),
search
);
const [res, setReset] = useState(false);
const reset = () => setReset(!setStatusFilters);
const objsWithLocations = resultObjs.filter(
({ location }) =>
location?.[0] !== null &&
location?.[0] !== undefined &&
location?.[1] !== null &&
location?.[1] !== undefined
);
const defaultFilterState = useMemo(
() => makeDefaultFilterState(trackingObjects),
[trackingObjects]
);
const filterOptionsObj = useMemo(
() => makeFilterOptionsObj(trackingObjects),
[trackingObjects]
);
const resetAllFilters = () => {
setStatusFilters([]);
};
// console.log('objsWithLocations', objsWithLocations);
return (
<Grid container direction="column" sx={classes.Root} wrap="nowrap">
<Stack
sx={{
flexDirection: { xs: "row", md: "column" },
alignItems: { xs: "center", md: "flex-start" },
justifyContent: { xs: "space-between", md: "flex-start" },
px: 1.0,
mb: 2,
}}
>
<TextInput
autoComplete="off"
variant="standard"
placeholder="Search for an object...."
value={search}
onValueChange={setSearch}
InputProps={{
startAdornment: <Search sx={classes.SearchLogo} />,
disableUnderline: true,
}}
sx={classes.SearchInput}
/>
<Divider sx={classes.Divider} />
<TrackingFilterContainer
defaultFilterState={defaultFilterState}
filterOptionsObj={filterOptionsObj}
/>
</Stack>
{loading && !resultObjs?.length && <Loading />}
{error && (
<Typography
variant="h5"
color="text.secondary"
mx="auto"
mt="5%"
width="45%"
textAlign="center"
>
{error}
</Typography>
)}
{!loading && objsWithLocations?.length === 0 ? (
<Typography
variant="h5"
color="text.secondary"
mx="auto"
mt="5%"
width="45%"
textAlign="center"
// onClick={() => defaultFilterState()}
>
No objects found based on your filter, try changing filter criteria
<Button
variant="text"
text="Back"
sx={{
fontSize: "24px",
p: 0,
verticalAlign: "baseline",
ml: "6px",
textTransform: "none",
}}
onClick={() => resetAllFilters()}
/>
</Typography>
) : (
<List sx={classes.ListWrapper}>
{objsWithLocations.map((obj) => (
<ObjectListItem
key={obj.trackingObjectId}
object={obj}
handleClick={handleClick}
isActive={selectedObj?.trackingObjectId === obj.trackingObjectId}
/>
))}
</List>
)}
</Grid>
);
}
export default ObjectList;
// export default React.memo(ObjectList);
Here is ObjectList.js rendered
import {Grid} from "#mui/material";
import {useNavigate} from "react-router-dom";
import {useTheme} from "#mui/material/styles";
import useMediaQuery from "#mui/material/useMediaQuery";
import React, {useEffect, useState} from "react";
import {TrackingPageItems} from "../../data/constants/TrackingObject";
import Map from "./map/Map";
import MobileNavigate from "./mobileNavigate/MobileNavigate";
import ObjectList from "./objectList/ObjectList";
import ObjectOne from "./objectOne/ObjectOne";
import buildSocketIoConnection from "../../data/socket.io/client";
import API from "../../data/api/API";
import {handleError} from "../../data/helpers/apiHelpers";
import {checkAuthStatus} from "../../data/helpers/Helpers";
import {filterObjects, filterSearch} from "../../data/helpers/Helpers";
function TrackingPage({
trackingObjects,
updateTrackingObjects,
setTrackingObjects,
selectedObject,
setSelectedObject,
...props
}) {
const [objectListSearch, setObjectListSearch] = useState("");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const [selectedTab, setSelectedTab] = useState(TrackingPageItems.LIST);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const navigate = useNavigate();
const handleTabChange = (newTab) => {
setSelectedTab(newTab);
};
useEffect(() => {
const userInfoString = localStorage.getItem("userInfo");
const userInfo = JSON.parse(userInfoString);
const token = userInfo?.token;
let possibleSocketIoConnection = null;
if (token) {
possibleSocketIoConnection = buildSocketIoConnection(token, updateTrackingObjects);
} else {
console.error("No token provided. Won't connect to socket.io server");
}
return () => {
setSelectedObject(null);
possibleSocketIoConnection?.closeConnection?.();
};
}, []);
const fetchObjectList = React.useCallback((firstTime = false) => {
firstTime && setLoading(true);
API.object
.getObjectList()
.then((objs) => {
// console.log('TrackingObjects', objs.data);
setTrackingObjects(objs.data);
})
.catch((err) => {
console.log(err);
checkAuthStatus(err.response?.status, navigate);
setError(handleError(err, "getObjectList"));
})
.finally(() => {
firstTime && setLoading(false)
});
}, []);
useEffect(() => {
fetchObjectList(!trackingObjects?.length);
// const interval = setInterval(fetchObjectList, 20000);
// return () => clearInterval(interval);
}, []);
const resultObjs = filterSearch(filterObjects(trackingObjects, props.statusFilters), objectListSearch);
return (
<>
<MobileNavigate selectedTab={selectedTab} handleTabChange={handleTabChange} />
<Grid container direction="row" spacing={1}>
{(!isMobile || (isMobile && selectedTab === TrackingPageItems.LIST)) && (
<Grid item container direction="column" xs={12} md={3}>
{selectedObject ? (
<ObjectOne trackingObjects={trackingObjects} selectedObj={selectedObject} setSelectedObj={setSelectedObject} />
) : (
<ObjectList
search={objectListSearch}
setSearch={setObjectListSearch}
selectedObj={selectedObject}
setSelectedObj={setSelectedObject}
trackingObjects={trackingObjects}
loading={loading}
error={error}
{...props}
/>
)}
</Grid>
)}
{(!isMobile || (isMobile && selectedTab === TrackingPageItems.MAP)) && (
<Grid item container direction="column" xs={12} md={9}>
<Map
loading={loading}
selectedObj={selectedObject}
setSelectedObj={setSelectedObject}
trackingObjects={resultObjs}
{...props}
/>
</Grid>
)}
</Grid>
</>
);
}
export default React.memo(TrackingPage);
How can I solve this error?

How to reload card in react on data submit?

I want to display the card when I submit data for the card. But I have to refresh the page to see changes I want to auto-refresh that card component on submitting data.
I have used use effect for that but still, it's not working. The value becoming true but data is not displayed. Still, I have to refresh the page. How can i solve this issue?
import { TextField, Box, Typography, Button, Link, DialogContent, CircularProgress, Dialog, DialogTitle, DialogContentText, DialogActions } from '#mui/material';
import React, { useEffect } from 'react';
import Cards from 'react-credit-cards';
import 'react-credit-cards/es/styles-compiled.css';
import { useContext, useState } from "react";
import { CardData } from '../../services/user-controller';
import { StatusCodes } from 'http-status-codes';
import { addCard, cardDelete, getCardList } from '../../services/card-controller';
import { ApplicationContext } from '../../context/applicationCtx';
import { nanoid } from 'nanoid';
import CancelIcon from '#mui/icons-material/Cancel';
import styles from "./mystyle.module.css"
import loadCardList from "../../pages/auth/main-app.js"
export default function PaymentForm() {
const {
getCardsList_CTX, setCardList_CTX, getReloadModuleData, setReloadModuleData
} = useContext(ApplicationContext);
const [error, setError] = useState('');
const [getLoaderState, setLoaderState] = useState(false);
const [cardClick, setCardClick] = useState(false);
const [cardNumber, setCardNumber] = useState('');
const [cardName, setCardName] = useState('');
const [cardExpiry, setCardExpiry] = useState('');
const [cardCvv, setCardCvv] = useState('');
const [focus, setFocus] = useState('');
const [cardId, setCardId] = useState('');
// const [inpval, setInpval] = useState('');
const [cardn, setCardn] = useState('');
const [carde, setCarde] = useState('');
const [open, setOpen] = useState(false);
const [deleteCard, setDeleteCard] = useState(null);
const [reloadData,setReloadData] = useState(false);
const handleClickOpen = (card) => {
setOpen(true);
setDeleteCard(card)
};
const handleClose = () => {
setOpen(false);
};
const handleDeleteCard = async function (){
let cardNum = `000000000000${deleteCard.cardNo.substring(4,8)}`
try {
setLoaderState(true);
let response = await cardDelete({
"cardNo": cardNum,
"expiryMonth": deleteCard.expiryMonth,
"expiryYear": deleteCard.expiryYear
})
if(response.status === 200 || response.status === 202 ){
setLoaderState(false);
setOpen(false)
}
} catch (err) {
setLoaderState(false);
if (err.data) {
//TODO:: ADD ERROR MESSAGES setStepLicenseKey({ ...getStepLicenseKey, errorMsg: err.data.message, hasError: true });
}else {
if (err.errorMsg) {
// setStepLicenseKey(err)
console.log(err.errorMsg)
} else {
console.error("UNKNOWN ERROR", err);
}
}
}
}
const handleChange = prop => (e) => {
switch (prop) {
case 'cardNumber':
setCardNumber(e.target.value)
break;
case 'cardName':
setCardName(e.target.value)
break;
case 'cardExpiry':
setCardExpiry(e.target.value)
break;
case 'cardCvv':
setCardCvv(e.target.value)
break;
case 'cardId':
setCardId(e.target.value)
break;
default:
break;
}
}
const handleInputFocus = (e) => {
setFocus( e.target.name );
}
const handleCardClick = (cardDetail, i) => {
setCardClick(true)
}
const delCancel = () => {
setCardClick(false)
}
const verifyInputs = async function () {
try {
setLoaderState(true);
const month = cardExpiry.substring(0, 2);
const year = `20${cardExpiry.substring(2)}`;
let response = await addCard({
"cardHolderName": cardName,
"cardNo": cardNumber,
"cvv": cardCvv,
"expiryMonth": month,
"expiryYear": year,
"cardId": cardNumber
})
if(response.status === StatusCodes.BAD_REQUEST){
setError(response.data.message)
}
// console.log(response,'rt')
else if (response.status === StatusCodes.OK) {
console.log(response)
setLoaderState(false)
setReloadData(true)
loadCardList();
} else {
setLoaderState(false)
throw (response);
}
} catch (err) {
setLoaderState(false);
if (err.data) {
//TODO:: ADD ERROR MESSAGES setStepLicenseKey({ ...getStepLicenseKey, errorMsg: err.data.message, hasError: true });
}
else {
if (err.errorMsg) {
// setStepLicenseKey(err)
console.log(err.errorMsg)
} else {
console.error("UNKNOWN ERROR", err);
}
}
}
}
// console.log(getCardsList_CTX,'yy')
useEffect(()=>{
if(reloadData == true){
getCardList();
}
console.log(reloadData)
},[reloadData])
return (
<>
<Box display="flex" sx={{
"width":"1459px","height":"44px","backgroundColor":"#F9FAFA","position":"relative","top":"0px",left:'77px',
}}></Box>
{/* TODO ADD CARDS HERE */}
<Box display="flex" sx={{
"width":"621px","height":"192px","position":"relative","top":"121px",left:'99px',gap:'10px'
}}>
{getCardsList_CTX.map((card, index)=>{
return (
<Box width="292px" height="184px" className={styles.card} onClick={() => handleCardClick(card, index)}>
{/* <CancelIcon color="error" sx={{ zIndex: 1, position: 'relative', left: '74%', top: '1%' }} /> */}
<Cards
key={nanoid()}
cvc={card.cvv}
expiry={`${card.expiryMonth < 9 ? '0' + card.expiryMonth : card.expiryMonth}${card.expiryYear.toString().substring(2)}`}
name={card.cardHolderName}
number={`000000000000${card.cardNo.substring(4)}`}
issuer={'visa'}
preview={true}
/>
{/* <Box position="absolute" width="100%" minHeight="100%" maxHeight="100%" bgcolor="red" zIndex='1'>
</Box> */}
<Box position="absolute" zIndex='2' onClick={() => {handleClickOpen(card)}}>
<CancelIcon sx={{ zIndex: 1,"position":"absolute","left":"263px","top":"-180px",color:"#dbd4d4",cursor:'pointer' }} />
</Box>
</Box>
)
}) }
</Box>
<Box sx={{ "width": "1536px", "height": "1px", "backgroundColor": "#ece5e5", "position": "absolute", "top": "385px" }}></Box>
<Typography mt={5} variant="h3" component="h2" sx={{ "position": "absolute", "top": "24px", "fontSize": "29px", "left": "97px" }}>
PAYMENT METHOD
</Typography>
<Typography mt={5} variant="h3" component="h2" sx={{ "position": "absolute", "top": "69px", "fontSize": "24px", "left": "108px", color: '#9C9C9C' }}>
YOUR SAVED CARDS
</Typography>
<Box style={{ "position": "absolute", "display": "flex", "top": "431px", "left": "418px" }} id="PaymentForm">
<Typography mt={5} variant="h3" component="h2" sx={{ whiteSpace:'nowrap', "position": "absolute", "top": "-79px", "fontSize": "20px", "left": "307px", color: '#9C9C9C' }}>
ENTER CARD DETAILS - SAVE/DELETE
</Typography>
<Cards
cvc={cardCvv}
expiry={cardExpiry}
focused={focus}
name={cardName}
number={cardNumber}
sx={{ "marginRight": "12px" }}
/>
<form style={{ width: '333px', marginLeft: '18px' }}>
<TextField type="tel"
name="number"
placeholder="Card Number"
onChange={handleChange('cardNumber')}
onFocus={handleInputFocus} id="outlined-basic" label="Card Number" variant="outlined"
sx={{ width: '340px' }}
inputProps={{ maxLength: 16 }}
/>
<TextField
type="tel"
name="name"
placeholder="Name"
onChange={handleChange('cardName')}
onFocus={handleInputFocus}
id="outlined-basic" label="Card Holder" variant="outlined"
sx={{ "marginTop": "12px", width: '340px' }}
/>
<Box sx={{ "display": "flex", "marginTop": "12px", width: '359px' }}>
<TextField
type="tel"
name="expiry"
placeholder="Expiry"
onChange={handleChange('cardExpiry')}
onFocus={handleInputFocus}
id="outlined-basic" label="Expiry" variant="outlined"
sx={{ "display": "flex", marginRight: '16px', width: '340px' }}
inputProps={{ maxLength: 4 }}
/>
<TextField
type="tel"
name="cvc"
placeholder="CVV"
onChange={handleChange('cardCvv')}
onFocus={handleInputFocus}
id="outlined-basic" label="CVV" variant="outlined"
inputProps={{ maxLength: 3 }}
sx={{ marginRight: '19px' }}
/>
</Box>
</form>
<Typography mt={5} variant="h3" component="h2" sx={{ whiteSpace:'nowrap', "position": "absolute", "top": "160px", "fontSize": "10px", "left": "312px", color: 'red',textTransform:'uppercase',fontWeight: 'bold',letterSpacing:'2px' }}>
{error}
</Typography>
<Button disabled={getLoaderState} onClick={verifyInputs} variant="contained" disableElevation sx={{ mt: 1, pr: 2 ,"marginTop":"219px","width":"159px","height":"43px","marginLeft":"-241px"}} >
Save Card {getLoaderState ? <CircularProgress size="1.2em" sx={{ "position": "absolute", right: "0", mr: 2 }} /> : ""}
</Button>
{/* Payment Card Delete */}
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Delete Payment Card"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete your card({deleteCard !== null ? deleteCard.cardNo : ''}), you can't retrive it later on.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} sx={{ color: 'red' }}>Cancel</Button>
<Button onClick={handleDeleteCard}>
Proceed
</Button>
</DialogActions>
</Dialog>
</Box>
</>
);
}
You need to notify the context that the content has updated. As for React Contact api you would need to change the state of the provider.
Here is an article that shows how to use the context api.
https://kentcdodds.com/blog/how-to-use-react-context-effectively

How to update total price of products on React functional components?

I'm creating a e-commerce website. But i'm unable to update the total price using the useState hook of react.
When is use forEach to update total price it give me this error: Uncaught (in promise) Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
codes:
import { Container, Grid, Typography } from "#mui/material";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import useAuth from "../../../hooks/useAuth";
import Cart from "../Cart/Cart";
import CartProduct from "../CartProduct/CartProduct";
const CartContainer = () => {
const { user } = useAuth();
// useNavigate hook
const navigate = useNavigate();
/* states */
const [products, setProducts] = useState([]);
const [quantity, setQuantity] = useState(0);
const [isDeleted, setIsDeleted] = useState(false);
const [total, setTotal] = useState(0);
const handleAddQuantity = () => {
setQuantity(quantity + 1);
};
const handleRemoveQuantity = () => {
if (quantity === 0) {
return;
} else {
setQuantity(quantity - 1);
}
};
const handleDelete = (id) => {
fetch(`https://limitless-crag-38673.herokuapp.com/cart?id=${id}`, {
method: "DELETE",
})
.then((res) => res.json())
.then((data) => {
data.deletedCount > 0 && setIsDeleted(true);
alert("successfully deleted");
});
};
useEffect(() => {
setIsDeleted(false);
fetch(
`https://limitless-crag-38673.herokuapp.com/cart/product?email=${user.email}`
)
.then((res) => res.json())
.then((data) => setProducts(data));
}, [isDeleted]);
return (
<>
{/* main cart */}
<Container sx={{ my: 3 }} maxWidth="xl">
<Grid container spacing={{ xs: 1, md: 2 }}>
{/* products */}
<Grid item md={8} xs={12}>
{/* cart title */}
<Grid
container
sx={{
borderBottom: "1px solid gray",
display: { xs: "none", sm: "flex" },
}}
>
<Grid item md={6}>
<Typography variant="h5">Product</Typography>
</Grid>
<Grid item md={4}>
<Typography variant="h5" sx={{ textAlign: "center" }}>
Price
</Typography>
</Grid>
{/* <Grid item md={2}>
<Typography variant="h5" sx={{ textAlign: "center" }}>
Quantity
</Typography>
</Grid>
<Grid item md={2}>
<Typography variant="h5" sx={{ textAlign: "center" }}>
Subtotal
</Typography>
</Grid> */}
<Grid item md={2}>
<Typography variant="h5" sx={{ textAlign: "center" }}>
Cancel
</Typography>
</Grid>
</Grid>
{/*============
product
============= */}
{products.map((product) => (
<CartProduct
key={product._id}
product={product}
handleDelete={handleDelete}
total={total}
setTotal={setTotal}
/>
))}
</Grid>
{/*===========
cart
================ */}
<Cart />
</Grid>
</Container>
</>
);
};
export default CartContainer;
`And when I try this it gives me this error: Uncaught (in promise) Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
codes:
import CancelOutlinedIcon from "#mui/icons-material/CancelOutlined";
import { Grid, IconButton, Typography } from "#mui/material";
import { Box } from "#mui/system";
import React from "react";
const CartProduct = ({ product, handleDelete, setTotal, total }) => {
const { name, price, src } = product;
setTotal(total + price);
return (
<Grid
container
sx={{
borderBottom: "1px solid gray",
display: "flex",
flexDirection: { xs: "column", md: "row" },
alignItems: "center",
}}
spacing={{ md: 2, xs: 1 }}
>
{/* product */}
<Grid
sx={{
display: "flex",
alignItems: "center",
flexDirection: { md: "row", xs: "column" },
}}
item
md={6}
xs={12}
>
<Box>
<img src={src} alt="" />
</Box>
<Typography sx={{ ml: 1 }} variant="h6">
{name}
</Typography>
</Grid>
{/* Price */}
<Grid
item
md={4}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex",
}}
>
<Typography variant="h5">$ {price}</Typography>
</Grid>
{/*
// quantity
<Grid
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
item
md={2}
xs={12}
>
<IconButton onClick={handleRemoveQuantity}>
<RemoveCircleOutlineOutlinedIcon />
</IconButton>
<TextField value={quantity} />
<IconButton onClick={handleAddQuantity}>
<AddCircleOutlineOutlinedIcon />
</IconButton>
</Grid>
// Subtotal
<Grid
item
md={2}
xs={12}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Typography variant="h5">$ {product.price}</Typography>
</Grid> */}
{/* Cancel */}
<Grid
item
md={2}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<IconButton onClick={() => handleDelete(product._id)}>
<CancelOutlinedIcon fontSize="large" />
</IconButton>
</Grid>
</Grid>
);
};
export default CartProduct;
Why not calculate Total in parent component?
because setState automatically triggers re-render and in your case every rerender you trigger another setState.
if you want to store data somewhere without re-render use useRef.
But in your case i would suggest to set Total price in CartContainer
You don't need total and subTotal, you can directly calculate it from product.
The below code need to be in CartProduct as you want to apply delete on individual items.
const [isDeleted, setIsDeleted] = useState(false);
useEffect(() => {
setIsDeleted(false);
fetch(
`https://limitless-crag-38673.herokuapp.com/cart/product?email=${user.email}`
)
.then((res) => res.json())
.then((data) => setProducts(data));
}, [isDeleted]);

How to handle multiple menu state with Material-UI Menu component?

I have a dynamically generated set of dropdowns and accordions that populate at render client-side (validated user purchases from db).
I'm running into an error that I'm sure comes from my menu anchorEl not knowing 'which' menu to open using anchorEl. The MUI documentation doesn't really cover multiple dynamic menus, so I'm unsure of how to manage which menu is open
Here is a pic that illustrates my use-case:
As you can see, the menu that gets anchored is actually the last rendered element. Every download button shows the last rendered menu. I've done research and I think I've whittled it down to the anchorEl and open props.
Here is my code. Keep in mind, the data structure is working as intended, so I've omitted it to keep it brief, and because it's coming from firebase, I'd have to completely recreate it here (and I think it's redundant).
The component:
import { useAuth } from '../contexts/AuthContext'
import { Accordion, AccordionSummary, AccordionDetails, Button, ButtonGroup, CircularProgress, ClickAwayListener, Grid, Menu, MenuItem, Typography } from '#material-ui/core'
import { ExpandMore as ExpandMoreIcon } from '#material-ui/icons'
import LoginForm from '../components/LoginForm'
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { db, functions } from '../firebase'
import styles from '../styles/Account.module.scss'
export default function Account() {
const { currentUser } = useAuth()
const [userPurchases, setUserPurchases] = useState([])
const [anchorEl, setAnchorEl] = useState(null)
const [generatingURL, setGeneratingURL] = useState(false)
function openDownloads(e) {
setAnchorEl(prevState => (e.currentTarget))
}
function handleClose(e) {
setAnchorEl(prevState => null)
}
function generateLink(prefix, variationChoice, pack) {
console.log("pack from generate func", pack)
setGeneratingURL(true)
const variation = variationChoice ? `${variationChoice}/` : ''
console.log('link: ', `edit-elements/${prefix}/${variation}${pack}.zip`)
setGeneratingURL(false)
return
if (pack.downloads_remaining === 0) {
console.error("No more Downloads remaining")
setGeneratingURL(false)
handleClose()
return
}
handleClose()
const genLink = functions.httpsCallable('generatePresignedURL')
genLink({
fileName: pack,
variation: variation,
prefix: prefix
})
.then(res => {
console.log(JSON.stringify(res))
setGeneratingURL(false)
})
.catch(err => {
console.log(JSON.stringify(err))
setGeneratingURL(false)
})
}
useEffect(() => {
if (currentUser !== null) {
const fetchData = async () => {
// Grab user products_owned from customers collection for user UID
const results = await db.collection('customers').doc(currentUser.uid).get()
.then((response) => {
return response.data().products_owned
})
.catch(err => console.log(err))
Object.entries(results).map(([product, fields]) => {
// Grabbing each product document to get meta (title, prefix, image location, etc [so it's always current])
const productDoc = db.collection('products').doc(product).get()
.then(doc => {
const data = doc.data()
const productMeta = {
uid: product,
title: data.title,
main_image: data.main_image,
product_prefix: data.product_prefix,
variations: data.variations
}
// This is where we merge the meta with the customer purchase data for each product
setUserPurchases({
...userPurchases,
[product]: {
...fields,
...productMeta
}
})
})
.catch(err => {
console.error('Error retrieving purchases. Please refresh page to try again. Full error: ', JSON.stringify(err))
})
})
}
return fetchData()
}
}, [currentUser])
if (userPurchases.length === 0) {
return (
<CircularProgress />
)
}
return(
currentUser !== null && userPurchases !== null ?
<>
<p>Welcome, { currentUser.displayName || currentUser.email }!</p>
<Typography variant="h3" style={{marginBottom: '1em'}}>Purchased Products:</Typography>
{ userPurchases && Object.values(userPurchases).map((product) => {
const purchase_date = new Date(product.purchase_date.seconds * 1000).toLocaleDateString()
return (
<motion.div key={product.uid}>
<Accordion style={{backgroundColor: '#efefef'}}>
<AccordionSummary expandIcon={<ExpandMoreIcon style={{fontSize: "calc(2vw + 10px)"}}/>} aria-controls={`${product.title} accordion panel`}>
<Grid container direction="row" alignItems="center">
<Grid item xs={3}><img src={product.main_image} style={{ height: '100%', maxHeight: "200px", width: '100%', maxWidth: '150px' }}/></Grid>
<Grid item xs={6}><Typography variant="h6">{product.title}</Typography></Grid>
<Grid item xs={3}><Typography variant="body2"><b>Purchase Date:</b><br />{purchase_date}</Typography></Grid>
</Grid>
</AccordionSummary>
<AccordionDetails style={{backgroundColor: "#e5e5e5", borderTop: 'solid 6px #5e5e5e', padding: '0px'}}>
<Grid container direction="column" className={styles[`product-grid`]}>
{Object.entries(product.packs).map(([pack, downloads]) => {
// The pack object right now
return (
<Grid key={ `${pack}-container` } container direction="row" alignItems="center" justify="space-between" style={{padding: '2em 1em'}}>
<Grid item xs={4} style={{ textTransform: 'uppercase', backgroundColor: 'transparent' }}><Typography align="left" variant="subtitle2" style={{fontSize: 'calc(.5vw + 10px)'}}>{pack}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}><Typography variant="subtitle2" style={{fontSize: "calc(.4vw + 10px)"}}>{`Remaining: ${downloads.downloads_remaining}`}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}>
<ButtonGroup variant="contained" fullWidth >
<Button id={`${pack}-btn`} disabled={generatingURL} onClick={openDownloads} color='primary'>
<Typography variant="button" style={{fontSize: "calc(.4vw + 10px)"}} >{!generatingURL ? 'Downloads' : 'Processing'}</Typography>
</Button>
</ButtonGroup>
<ClickAwayListener key={`${product.product_prefix}-${pack}`} mouseEvent='onMouseDown' onClickAway={handleClose}>
<Menu anchorOrigin={{ vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} id={`${product}-variations`} open={Boolean(anchorEl)} anchorEl={anchorEl}>
{product.variations && <MenuItem onClick={() => generateLink(product.product_prefix, null, pack) }>{`Pack - ${pack}`}</MenuItem>}
{product.variations && Object.entries(product.variations).map(([variation, link]) => {
return (
<MenuItem key={`${product.product_prefix}-${variation}-${pack}`} onClick={() => generateLink(product.product_prefix, link, pack)}>{ variation }</MenuItem>
)
})}
</Menu>
</ClickAwayListener>
</Grid>
</Grid>
)}
)}
</Grid>
</AccordionDetails>
</Accordion>
</motion.div>
)
})
}
</>
:
<>
<p>No user Signed in</p>
<LoginForm />
</>
)
}
I think it also bears mentioning that I did check the rendered HTML, and the correct lists are there in order - It's just the last one assuming the state. Thanks in advance, and please let me know if I've missed something, or if I can clarify in any way. :)
i couldn't manage to have a menu dynamic,
instead i used the Collapse Panel example and there i manipulated with a property isOpen on every item of the array.
Check Cards Collapse Example
On the setIsOpen method you can change this bool prop:
const setIsOpen = (argNodeId: string) => {
const founded = tree.find(item => item.nodeId === argNodeId);
const items = [...tree];
if (founded) {
const index = tree.indexOf(founded);
founded.isOpen = !founded.isOpen;
items[index]=founded;
setTree(items);
}
};
<IconButton className={clsx(classes.expand, {
[classes.expandOpen]: node.isOpen,
})}
onClick={()=>setIsOpen(node.nodeId)}
aria-expanded={node.isOpen}
aria-label="show more"
>
<MoreVertIcon />
</IconButton>
</CardActions>
<Collapse in={node.isOpen} timeout="auto" unmountOnExit>
<CardContent>
<MenuItem onClick={handleClose}>{t("print")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkContainers")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkDetails")}</MenuItem>
</CardContent>
</Collapse>
I think this is the right solution for this: https://stackoverflow.com/a/59531513, change the anchorEl for every Menu element that you render. :D
This code belongs to TS react if you are using plain JS. Then remove the type.
import Menu from '#mui/material/Menu';
import MenuItem from '#mui/material/MenuItem';
import { useState } from 'react';
import { month } from '../../helper/Utilities';
function Company() {
const [anchorEl, setAnchorEl] = useState<HTMLElement[]>([]);
const handleClose = (event: any, idx: number) => {
let array = [...anchorEl];
array.splice(idx, 1);
setAnchorEl(array);
};
<div>
{month &&
month.map((val: any, ind: number) => {
return (
<div
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient'}
style={{ borderColor: ind === 0 ? '#007B55' : '#919EAB52' }}
>
<Menu
id='demo-positioned-menu'
aria-labelledby='demo-positioned-button'
anchorEl={anchorEl[ind]}
open={anchorEl[ind] ? true : false}
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span
style={{
marginLeft: '.5em',
color: 'black',
background: 'inherit',
}}
>
Make Primary
</span>
</MenuItem>
<MenuItem onClick={(event) => handleClose(event, ind)}>
<span style={{ marginLeft: '.5em', color: 'black' }}>Edit</span>
</MenuItem>
<MenuItem
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span style={{ marginLeft: '.5em', color: 'red' }}>Delete</span>
</MenuItem>
</Menu>
</div>
);
})}
</div>;
}
export default Company;

Categories