Showing Circular indeterminate Progress on button click - reactjs

I have a button that when clicked runs some code that imports data from another website. What would be nice is to have a Circular indeterminate indicator (Material UI) that shows the import is in progress, until completion.
My code below shows the code I am running and some of the jsx.
<Box mt={5} mb={5} width={1200}>
<Grid
container
justify="center"
spacing={2}
style={{ padding: '25px' }}
>
<Grid item xs={7}>
<Typography variant="h3" style={{ fontWeight: 600 }}>
List
</Typography>
<Typography variant="h5" style={{ marginTop: '2em' }}>
some text
</Typography>
<Button
variant="contained"
color="primary"
size="large"
style={{ marginTop: '1em' }}
// style={{ maxWidth: '108px', minWidth: '108px' }}
onClick={() => {
let arrCollection = [];
const stream = fetch(
'https://othersite.org/api/games/user/neio',
{ headers: { Accept: 'application/x-ndjson' } }
);
const onMessage = obj => {
arrCollection.push(obj);
};
const onComplete = () =>
console.log('The stream has completed');
stream.then(readStream(onMessage)).then(onComplete);
console.log('arrCollection', arrCollection);
}}
>
Import Games
</Button>
</Grid>
</Grid>
</Box>
What I can't work out is how to display the "circular indeterminate" while importing and then show it completed.
This is the link to the circular indeterminate code: https://material-ui.com/components/progress/
Do I need to add a node to the Dom or something?

As mentioned already, you can use state to manage this.
Something like this should work
const [loading, setLoading] = useState(false);
<div>{loading ? <LoadingIndicator /> : ''}</div>
<Button
onClick={() => {
setLoading(true);
let arrCollection = [];
const stream = fetch('https://othersite.org/api/games/user/neio', {
headers: { Accept: 'application/x-ndjson' },
});
const onMessage = (obj) => {
arrCollection.push(obj);
};
const onComplete = () => console.log('The stream has completed');
stream.then(readStream(onMessage)).then(onComplete);
console.log('arrCollection', arrCollection);
setLoading(false);
}}
>
Import Games
</Button>

Related

AutoComplete: get the id for the user that i was choosen from users list, post request with status 400

I have this project and as it is clear in the postman, I have a request and through the request I must send the invoiceId to the invoice and the user ID must be sent in addition to a message, but the real problem is that I did not know how to get the user ID, the meaning is that when I printed “invoiceID” , "UserID "and "Message", the invoiceID and the message have a value, but UserID is worthless without value "undefined", and the reason is that I have list of the Users, and I must choose one user and then I want to pass this userID for the user that i was choosen and pass it in the "assignToUser" function, and i don't know how to do that.
And in the network I have these errors:
and in Network i have this errors:
enter image description here
how can i solve my problem?
invoiceSlice.js:
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
import FuseUtils from "#fuse/utils";
import { getInvoices } from "./invoicesSlice";
export const assignToUser = createAsyncThunk(
"invoicesApp/invoice/assignToUser",
async ({ invoiceId, userId, message }, { dispatch }) => {
console.log("invoiceId, userId, message", invoiceId, userId, message);
const response = await axios
.post(`/invoices/flow/${invoiceId}/approve`, { userId, message })
.catch((error) => {
console.log("error response: ", error);
});
const data = await response.data.data;
console.log("approve invoices: ", data);
dispatch(getInvoices());
return data;
}
);
const invoiceSlice = createSlice({
name: "invoicesApp/invoice",
initialState: null,
reducers: {
resetInvoice: () => null,
newInvoice: {
reducer: (state, action) => action.payload,
prepare: (event) => ({
payload: {
invoice: "",
netAmount: 0,
taxNumber: 0,
grossAmount: 0,
dueDate: "",
issueDate: "",
},
}),
},
},
extraReducers: {
[assignToUser.fulfilled]: (state, action) => action.payload,
},
});
export const { newInvoice, resetInvoice } = invoiceSlice.actions;
export default invoiceSlice.reducer;
approveUser.js:
import { Fragment, useState } from "react";
import { ButtonGroup } from "#material-ui/core";
import React from "react";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import DialogActions from "#material-ui/core/DialogActions";
import DialogContent from "#material-ui/core/DialogContent";
import DialogContentText from "#material-ui/core/DialogContentText";
import DialogTitle from "#material-ui/core/DialogTitle";
import useMediaQuery from "#material-ui/core/useMediaQuery";
import { useTheme } from "#material-ui/core/styles";
import { useSnackbar } from "notistack";
import Slide from "#material-ui/core/Slide";
import {
rejectInvoice,
approveInvoice,
assignToUser,
} from "../../store/invoiceSlice";
import { useDispatch, useSelector } from "react-redux";
import TextField from "#mui/material/TextField";
import FlagIcon from "#mui/icons-material/Flag";
import { makeStyles } from "#material-ui/core/styles";
import Autocomplete from "#mui/material/Autocomplete";
import { getUsers } from "../../store/invoiceSlice";
import { useEffect } from "react";
const useStyles = makeStyles((theme) => ({
paper: { padding: "3rem", maxWidth: "990px", minWidth: "300px" },
textStyle: {
paddingLeft: "2rem",
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
font: {
fontSize: "5rem",
},
}));
const GroupButttonApproveStatus = (id) => {
const [dialogOpen, setDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [assignToUserDialog, setAssignToUserDialog] = useState(false);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const dispatch = useDispatch();
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
const AssignToUserFullScreen = useMediaQuery(theme.breakpoints.down("md"));
const classes = useStyles();
const [users, setUsers] = useState([]);
const [message, setMessage] = useState("");
useEffect(() => {
getUsers().then((response) => {
setUsers(response);
});
}, []);
// confirm
console.log("users: ", users);
const handleAssignToUserDialogOpen = () => {
setAssignToUserDialog(true);
};
const handleAssignToUserDialogClose = () => setAssignToUserDialog(false);
// end assign to user
const handleConfirmDialogClose = () => setConfirmDialogOpen(false);
const handleClickConfirmDialogOpen = () => {
setConfirmDialogOpen(true);
};
//end confirm
const handleDialogClose = () => setDialogOpen(false);
const handleClickOpen = () => {
setDialogOpen(true);
};
const handleClose = () => {
setDialogOpen(false);
};
const rejectInvoiceHandleClick = () => {
enqueueSnackbar(
"Invoice rejected successfully",
{ variant: "error" },
{
anchorOrigin: {
vertical: "top",
horizontal: "right",
},
},
{ TransitionComponent: Slide }
);
};
const approveInvoiceHandleClick = () => {
enqueueSnackbar(
"Invoice approved successfully",
{ variant: "success" },
{
anchorOrigin: {
vertical: "top",
horizontal: "right",
},
},
{ TransitionComponent: Slide }
);
};
return (
<Fragment>
<ButtonGroup size="large">
<Button
onClick={(ev) => {
handleClickConfirmDialogOpen();
}}
>
Approve
</Button>
<Button
onClick={(ev) => {
handleClickOpen();
}}
>
Reject
</Button>
<Button
onClick={(ev) => {
handleAssignToUserDialogOpen();
}}
>
Assign to User to approve
</Button>
</ButtonGroup>
{/* reject Dialog */}
<Dialog
classes={{ paper: classes.paper }}
maxWidth="sm"
fullScreen={fullScreen}
open={dialogOpen}
onClose={handleDialogClose}
>
<DialogTitle style={{ fontWeight: "bold" }}>Reject Invoice</DialogTitle>
<DialogContent>
<div
style={{
backgroundColor: "#F8F9FA",
borderRadius: 10,
padding: "3rem",
}}
>
<DialogContentText>
<FlagIcon
style={{ fontSize: 40, color: "#dc3c24", paddingRight: "1rem" }}
/>
Do you really want to reject this invoice ?
</DialogContentText>
<DialogContentText>
<FlagIcon
style={{ fontSize: 40, color: "#F8F9FA", paddingRight: "1rem" }}
/>
Keep in mind that once the invoice is rejected you won’t be able
to proceed with it.
</DialogContentText>
</div>
</DialogContent>
<DialogActions>
<div style={{ paddingRight: "1rem" }}>
<Button
onClick={handleClose}
style={{ color: "#dc3c24", fontWeight: 500 }}
autoFocus
>
Cancel
</Button>
<Button
onClick={(ev) => {
dispatch(rejectInvoice(id?.id));
rejectInvoiceHandleClick(ev);
handleClose();
}}
style={{ color: "#212529", fontWeight: 500 }}
color="primary"
autoFocus
>
Reject Invoice
</Button>
</div>
</DialogActions>
</Dialog>
{/* End reject Dialog */}
{/* Confirm Dialog */}
<Dialog
classes={{ paper: classes.paper }}
maxWidth="sm"
fullScreen={fullScreen}
open={confirmDialogOpen}
onClose={handleConfirmDialogClose}
>
<DialogTitle style={{ fontWeight: "bold" }}>
Approve Invoice
</DialogTitle>
<DialogContent>
<div
style={{
backgroundColor: "#F8F9FA",
borderRadius: 10,
padding: "3rem",
}}
>
<DialogContentText>Almost ready for payment !</DialogContentText>
<DialogContentText>
By confirming you mark this invoice ready for approval.
</DialogContentText>
</div>
</DialogContent>
<DialogActions>
<div style={{ paddingRight: "1rem" }}>
<Button
onClick={handleConfirmDialogClose}
style={{ color: "#dc3c24", fontWeight: 500 }}
autoFocus
>
Cancel
</Button>
<Button
onClick={(ev) => {
dispatch(approveInvoice(id?.id));
approveInvoiceHandleClick(ev);
handleConfirmDialogClose();
}}
style={{ color: "#212529", fontWeight: 500 }}
color="primary"
autoFocus
>
yes, Confirm
</Button>
</div>
</DialogActions>
</Dialog>
{/* End Confirm Dialog */}
{/* assign to user dialog */}
<Dialog
classes={{ paper: classes.paper }}
maxWidth="sm"
fullScreen={AssignToUserFullScreen}
open={assignToUserDialog}
onClose={handleAssignToUserDialogClose}
>
<DialogTitle style={{ fontWeight: "bold", fontSize: "3rem" }}>
Request approval
</DialogTitle>
<div
style={{
backgroundColor: "#F8F9FA",
borderRadius: 10,
padding: "2rem",
paddingLeft: "2rem",
}}
>
<DialogContentText style={{ fontWeight: 600 }}>
{" "}
<FlagIcon
style={{ fontSize: 40, color: "#aacc00", paddingRight: "1rem" }}
/>
Send an invoice approval request to a team member.
</DialogContentText>
<DialogContentText style={{ paddingLeft: 10 }}>
The assigned member will receive a notification asking them to
approve this invoice. Once they accept, payment is on the way!
</DialogContentText>
</div>
<DialogTitle>Assign a member to approve</DialogTitle>
<DialogContent>
<Autocomplete
id="combo-box-demo"
// value={users || ""}
options={users || []}
getOptionLabel={(option) => option.name || ""}
sx={{ width: 860 }}
renderInput={(params) => (
<TextField
{...params}
placeholder="Search Member"
fullWidth
InputProps={{ ...params.InputProps, style: { fontSize: 17 } }}
InputLabelProps={{ style: { fontSize: 17 } }}
/>
)}
/>
</DialogContent>
<DialogContent style={{ marginTop: "15rem" }}>
<form className={classes.root} noValidate autoComplete="off">
<TextField
value={message}
onChange={(e) => setMessage(e.target.value)}
id="outlined-basic"
variant="outlined"
placeholder="Add a message"
fullWidth
size="medium"
InputProps={{ style: { fontSize: 17 } }}
InputLabelProps={{ style: { fontSize: 17 } }}
/>
</form>
</DialogContent>
<DialogActions>
<div style={{ paddingRight: "1rem" }}>
<Button
onClick={handleAssignToUserDialogClose}
style={{ color: "#dc3c24", fontWeight: 500 }}
autoFocus
>
Cancel
</Button>
<Button
onClick={(ev) => {
dispatch(assignToUser(id?.id, users.id, message));
approveInvoiceHandleClick(ev);
handleAssignToUserDialogClose();
}}
style={{ color: "#212529", fontWeight: 500 }}
color="primary"
autoFocus
>
Assign to approve
</Button>
</div>
</DialogActions>
</Dialog>
</Fragment>
);
};
export default GroupButttonApproveStatus;
invoiceDetails.js:
import React from "react";
import { getInvoice } from "../../store/invoiceSlice";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import Grid from "#material-ui/core/Grid";
import TextField from "#material-ui/core/TextField";
import moment from "moment";
import InputAdornment from "#material-ui/core/InputAdornment";
import TodayIcon from "#material-ui/icons/Today";
import { makeStyles } from "#material-ui/core/styles";
import RejectDialog from "./rejectDialog";
import GroupButton from "./groupButttonReviewStatus";
import GroupButttonReviewStatus from "./groupButttonReviewStatus";
import GroupButttonApproveStatus from "./groupButtonApproveStatus";
import GroupButttonPaymentStatus from "./groupButtonPaymentStatus";
import useMediaQuery from "#material-ui/core/useMediaQuery";
import { useTheme } from "#material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
root: {
"& > *": {
margin: theme.spacing(1),
},
},
input: {
display: "none",
},
button: {
margin: theme.spacing(1),
// padding: theme.spacing(4),
},
}));
const InvoiceDetails = () => {
const classes = useStyles();
const theme = useTheme();
const routeParams = useParams();
const [invoice, setInvoice] = useState([]);
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);
const [selectedIndex, setSelectedIndex] = React.useState(1);
const breakpoint = useMediaQuery(theme.breakpoints.down("sm"));
// const defaultLayoutPluginInstance = defaultLayoutPlugin();
useEffect(() => {
getInvoice(routeParams).then((response) => {
setInvoice(response);
});
}, []);
const handleClick = () => {
console.info(`You clicked ${options[selectedIndex]}`);
};
const handleMenuItemClick = (event, index) => {
setSelectedIndex(index);
setOpen(false);
};
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
console.log("invoice url: ", invoice?.file?.url);
console.log("invoice tara : ", invoice);
const statusGropButton = (status, id) => {
switch (status) {
case "review_pending":
return <GroupButttonReviewStatus id={id} />;
case "approval_pending":
return <GroupButttonApproveStatus id={id} />;
case "payment_pending":
return <GroupButttonPaymentStatus id={id} />;
case "rejected":
return <GroupButton id={id} />;
default:
return;
}
};
return (
<>
<Grid container>
<Grid item xs={7} sm={7}>
{/* pdf viewer */}
<object
// data={invoice?.file?.url}
data="https://documentcloud.adobe.com/view-sdk-demo/PDFs/Bodea%20Brochure.pdf"
type="application/pdf"
width="100%"
height="100%"
>
<p>
Alternative text - include a link{" "}
<a href="https://documentcloud.adobe.com/view-sdk-demo/PDFs/Bodea%20Brochure.pdf">
to the PDF!
</a>
</p>
</object>
</Grid>
<Grid item xs={5} sm={5} style={{ padding: "4rem" }}>
<Grid item>
<h1 style={{ fontWeight: "bold" }}>Invoice Details</h1>
</Grid>
<Grid item style={{ marginTop: "3rem", marginBottom: "2rem" }}>
<Grid item style={{ marginBottom: 10 }}>
<h3>From</h3>
</Grid>
<Grid item>
<h3>{invoice?.submittedBy?.name || ""}</h3>
</Grid>
<Grid item>
<h3>{invoice?.submittedBy?.email || ""}</h3>
</Grid>
</Grid>
<Grid item>
<Grid container item direction={breakpoint ? "row" : "column"}>
<Grid
container
item
xs={3}
sm={3}
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<h3>Invoice ID</h3>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
className="mt-8 mb-16"
id="outlined-size-normal"
value={invoice.id || ""}
variant="outlined"
fullWidth
/>
</Grid>
</Grid>
<Grid container item direction={breakpoint ? "row" : "column"}>
<Grid
container
item
xs={3}
sm={3}
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<h3>Issue Date</h3>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
className="mt-8 mb-16"
id="outlined-size-normal"
value={
moment(moment.utc(invoice.issueDate).toDate())
.local()
.format("YYYY-MM-DD HH:mm:ss") || ""
}
variant="outlined"
InputProps={{
endAdornment: (
<InputAdornment position="start">
<TodayIcon />
</InputAdornment>
),
}}
fullWidth
/>
</Grid>
</Grid>
<Grid container item direction={breakpoint ? "row" : "column"}>
<Grid
container
item
xs={3}
sm={3}
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<h3>Due Date</h3>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
className="mt-8 mb-16"
id="outlined-size-normal"
value={
moment(moment.utc(invoice.dueDate).toDate())
.local()
.format("YYYY-MM-DD HH:mm:ss") || ""
}
variant="outlined"
InputProps={{
endAdornment: (
<InputAdornment position="start">
<TodayIcon />
</InputAdornment>
),
}}
fullWidth
/>
</Grid>
</Grid>
<Grid container item direction={breakpoint ? "row" : "column"}>
<Grid
container
item
xs={3}
sm={3}
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<h3>Net Amount</h3>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
className="mt-8 mb-16"
id="outlined-size-normal"
value={invoice.netAmount || ""}
variant="outlined"
fullWidth
/>
</Grid>
</Grid>
<Grid container item direction={breakpoint ? "row" : "column"}>
<Grid
container
item
xs={3}
sm={3}
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<h3>Tax Number</h3>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
className="mt-8 mb-16"
id="outlined-size-normal"
value={invoice.taxNumber || ""}
variant="outlined"
fullWidth
/>
</Grid>
</Grid>
<Grid container item direction={breakpoint ? "row" : "column"}>
<Grid
container
item
xs={3}
sm={3}
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<h3>Gross Amount</h3>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
className="mt-8 mb-16"
// label="Size"
id="outlined-size-normal"
value={invoice.grossAmount || ""}
variant="outlined"
fullWidth
/>
</Grid>
</Grid>
<Grid
container
direction="row"
justifyContent="center"
alignItems="center"
style={{ marginTop: "3rem" }}
>
<Grid item>{statusGropButton(invoice.status, invoice?.id)}</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</>
);
};
export default InvoiceDetails;
and this code:
useEffect(() => {
getUsers().then((response) => {
setUsers(response);
});
}, []);
return:
Based on your code I observed this.
You are already passing invoice?.id in statusGropButton(invoice.status, invoice?.id) method.
You are again expecting id from the input as id?.id in assignToUser(id?.id, users.id, message). Why?
Just assuming, because of that the invoiceId is going as undefined. Try by passing just id instead of id?.id while calling the api functions.
You are opening a Dialogue by using assignToUserDialog flag. While submitting the form, you are doing dispatch(assignToUser(id?.id, users.id, message)); in which users is an array. That is the reason you are getting undefined. I would suggest you maintain the specific user information for which the dialogue is opened and send that specifc user.id to the API call.
invoiceId undefined when send request ,you could check assignToUser

My react component return statement fails to render but console.log shows exactly what I need

I am new to react and working on a react video player. I'm having issue implementing the comment section.
This is my videoplayer component itself.
export default function VidPlayer() {
// useStates
const [state, setState] = useState({
playing: true,
});
const [comments, setComments] = useState([]);
const [comment, setComment] = useState("");
const { playing } = state;
const playerRef = useRef(null);
// declaring functions for video player buttons
const handlePlayPause = () => {
setState({ ...state, playing: !state.playing });
};
const handleRewind = () => {
playerRef.current.seekTo(playerRef.current.getCurrentTime() - 5);
};
const handleFoward = () => {
playerRef.current.seekTo(playerRef.current.getCurrentTime() + 5);
};
const handleStop = () => {
playerRef.current.seekTo(0);
setState({ playing: !state.playing });
};
// declaring functions for comment section
const addComments = () => {
if (comment) {
setComments({...comments, comment});
setComment("");
console.log("Hello", comments);
}
};
const handleComment = (e) => {
setComment(e.target.value);
};
return (
<div>
<AppBar style={{ background: "#e6880e" }} position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Favour's Video Player
</Typography>
</Toolbar>
</AppBar>
{/* container for the videoplayer, buttons and comment section */}
<div className="container">
<>
{/* videoplayer */}
<div className="reactPlayer one">
<ReactPlayer
ref={playerRef}
url="https://www.youtube.com/watch?v=1_ATK0BLc8U&t=3s"
playing={playing}
controls
/>
</div>
{/* buttons */}
<div className="btn-stack two">
<Stack spacing={2} direction="row">
<Button
style={{ background: "#e6880e" }}
size="small"
variant="contained"
onClick={handlePlayPause}
>
Play
</Button>
<Button
style={{ background: "#e6880e" }}
size="small"
variant="contained"
onClick={handleRewind}
>
Rewind{" "}
</Button>
<Button
style={{ background: "#e6880e" }}
size="small"
variant="contained"
onClick={handleFoward}
>
Forward{" "}
</Button>
<Button
style={{ background: "#e6880e" }}
size="small"
variant="contained"
onClick={handleStop}
>
Stop
</Button>
</Stack>
</div>
{/* comment section */}
<div className="comment three">
<Comment userComs={comments} />
<TextField
placeholder="add comment"
size="small"
variant="outlined"
onChange={handleComment}
value={comment}
/>
<Button
style={{ background: "#e6880e", marginLeft: '1em' }}
onClick={addComments}
variant="contained"
size="small"
>
Send
</Button>
</div>
</>
</div>
</div>
);
}
It takes in this comments component towards the end.
export default function commentList(props) {
console.log("Hello brian", props.userComs);
const { userComs } = props;
if (Object.keys(userComs).length > 0) {
console.log(userComs);
// userComs.forEach((element) => {
// console.log("Im in", userComs);
Object.values(userComs).forEach(val => {
// console.log("Im in", userComs);
// console.log(val);
return (
<div>
<List
sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper" }}
>
<ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt="Remy Sharp" src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
secondary={
<React.Fragment>
<Typography
sx={{ display: "inline" }}
component="span"
variant="body2"
color="text.primary"
>
Ali Connors
</Typography>
{val}
</React.Fragment>
}
/>
</ListItem>
<Divider variant="inset" component="li" />
</List>
</div>
);
});
} else {
return <div></div>;
}
}
This is the Front-End enter image description here
Unfortunately, when I enter a comment and click send, the screen goes blank and console throws a "nothing was returned from render" error. Can someone help me check what is wrong and how I can fix this please?
As the error says, the component isn't returning anything.
Object.values(userComs).forEach(val => {
should be
return Object.values(userComs).map(val => {
because forEach doesn't return anything and the JSX returned in each iteration will not be used anywhere, but map returns a new array that React can use.
BTW make sure to give a key prop to each div that is returned from the callback.
<div key={val}> // assuming `val` is unique

Undesired scrolling in React on rendering component

I have a component where the search results of a query are displayed. When clicking on a item to view its details, instead of loading normally (scrolled all the way up), the details page is, somehow, being affected by the scroll position of the results page. I know I could probably solve this by including window.scrollTo({ top: 0, left: 0 }), but I just want it to behave normally like it should and eliminate what's causing this issue.
Results.js
function Results() {
const matches = useMediaQuery("(max-width:767px)");
const navigate = useNavigate();
const [details, setDetails] = useState({ results: [] });
// Create a custom hook that binds to the `search` property from `location` url object
function useQuery() {
const { search } = useLocation();
return React.useMemo(() => new URLSearchParams(search), [search]);
}
let query = useQuery().toString();
useEffect(() => {
window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
// On successful response, assign the results to `results` array in `details` object
axios
.get(`/api/results/${query}`)
.then((response) => {
setDetails(response.data);
})
.catch((error) => {
console.log(error);
});
}, [query]);
const results = details.total_results;
const pages = details.total_pages;
const baseURL = "https://image.tmdb.org/t/p/w154";
const placeholderImg =
"https://www.genius100visions.com/wp-content/uploads/2017/09/placeholder-vertical.jpg";
return (
<CustomCard
title="Results"
content={
<div>
<Grid container>
<Typography>{results ? results : 0} results found</Typography>
<Grid item xs={12}>
<List sx={{ mt: 3 }}>
{details.results.map((result) => {
return (
<ResultItem
key={result.id}
id={result.id}
imgURL={
result.poster_path
? baseURL + result.poster_path
: placeholderImg
}
title={result.title}
year={result.release_date}
synopsis={result.overview}
/>
);
})}
</List>
</Grid>
</Grid>
<Pagination
sx={{ mt: 2 }}
count={pages}
siblingCount={matches ? 0 : 1}
onChange={(event, page) => {
const url = new URLSearchParams(window.location.search);
url.set("page", page);
const query = url.toString();
navigate(`/results?${query}`);
}}
/>
</div>
}
/>
);
}
export default Results;
ResultItems.jsx
function ResultItem(props) {
const navigate = useNavigate();
const matches = useMediaQuery("(max-width:767px)");
return (
<div>
<ListItem disablePadding>
<ListItemButton alignItems="flex-start" sx={{ pl: 0, pt: 2 }}>
<div>
<img
src={props.imgURL}
alt={`${props.title} poster`}
style={
matches
? { width: "80px", height: "120px" }
: { width: "154px", height: "231px" }
}
/>
</div>
<ListItemText
sx={{ ml: 3 }}
primary={props.title}
secondary={
<React.Fragment>
<Typography component="span" variant="body2">
{props.year.slice(0, 4)}
</Typography>
<br />
<br />
{matches ? null : (
<span style={{ textAlign: "justify" }}>{props.synopsis}</span>
)}
</React.Fragment>
}
onClick={() => navigate(`/details/${props.id}`)}
/>
</ListItemButton>
</ListItem>
<Divider />
</div>
);
}
export default ResultItem;
Details.js
function Details() {
const matches = useMediaQuery("(max-width:718px)");
const navigate = useNavigate();
const [details, setDetails] = useState({
tmdbDetails: { genres: [] },
tmdbCredits: { cast: [], crew: [] },
userData: { rating: null, review: "", view_count: null },
});
const [show, setShow] = useState(false);
const [isUpdated, setIsUpdated] = useState(false);
// GET MOVIE DETAILS AND USER LOGGED DATA
useEffect(() => {
const url = new URL(window.location.href);
axios
.get(`/api${url.pathname}`)
.then((response) => {
setDetails(response.data);
})
.catch((err) => {
console.log(err);
});
}, [isUpdated]);
const baseURL = "https://image.tmdb.org/t/p/w342";
const placeholderImg =
"https://www.genius100visions.com/wp-content/uploads/2017/09/placeholder-vertical.jpg";
// Expand section to log new diary entry or view/edit previous logged data
function handleExpand() {
setShow(!show);
}
// ENTRY DATA
const [userData, setUserData] = useState({
rating: null,
review: "",
date: new Date(),
});
// New object passing user data and movie data to be saved on database
const entryData = {
...userData,
title: details.tmdbDetails.title,
year: getYear(details),
director: getDirector(details),
genres: getGenres(details),
runtime: details.tmdbDetails.runtime,
};
// Control value on Date Picker and set it to `userData.date`
function handleDate(date) {
setUserData((prevValue) => {
return {
...prevValue,
date: date.toISOString(),
};
});
}
// Control value on Rating selector and set it to `userData.rating`
function handleRating(event, value) {
setUserData((prevValue) => {
return {
...prevValue,
rating: value,
};
});
}
// Control value on Text Area and set it to `userData.review`
function handleReview(event) {
const { value } = event.target;
setUserData((prevValue) => {
return {
...prevValue,
review: value,
};
});
}
// Submit entry to database and navigate to Diary
function onSubmit(event) {
event.preventDefault();
const url = new URL(window.location.href);
axios.post(`/api${url.pathname}`, entryData).then((res) => {
navigate("/diary");
});
}
// Function passed to the "WatchedPanel" component to be executed on saving changes after edit entry. It changes `isUpdated` state to force a re-render of `useEffect()` and update entry data on-screen
function handleUpdateDetails() {
setIsUpdated(!isUpdated);
}
return (
<Container component="main" maxWidth="md">
<Card sx={{ padding: matches ? 0 : 3, paddingBottom: 0, margin: 2 }}>
<div style={{ textAlign: "right" }}>
<IconButton
aria-label="close"
style={{ color: "#e0e0e0" }}
onClick={() => {
navigate(-1);
}}
>
<CloseIcon />
</IconButton>
</div>
<CardContent>
<Grid container alignItems="center">
{/* MOVIE TITLE & YEAR */}
<Grid item xs={12}>
<Typography variant="h5">{details.tmdbDetails.title}</Typography>
<Typography sx={{ mb: 2 }} variant="h6">
({getYear(details)})
</Typography>
</Grid>
{/* MOVIE POSTER */}
<Grid item xs={12} sm={matches ? 12 : 5} md={4}>
<div style={{ textAlign: matches ? "center" : "left" }}>
<Poster
source={
details.tmdbDetails.poster_path
? baseURL + details.tmdbDetails.poster_path
: placeholderImg
}
altText={`${details.tmdbDetails.title} poster`}
/>
</div>
</Grid>
{/* MOVIE DETAILS */}
<Grid item xs={12} sm={7} md={8}>
<Collapse in={matches ? show : true}>
<Credits
director={getDirector(details).join(", ")}
cast={getCast(details)}
genres={getGenres(details).join(", ")}
runtime={details.tmdbDetails.runtime}
/>
</Collapse>
</Grid>
<Grid item xs={12} sx={{ mt: 2 }}>
<Collapse in={matches ? show : true}>
<Typography style={{ fontWeight: "bold" }}>Synopsis</Typography>
<Typography>{details.tmdbDetails.overview}</Typography>
</Collapse>
</Grid>
{/* EXPAND SECTION BUTTON */}
{matches ? (
<Fab
color="primary"
size="small"
aria-label="expand"
sx={{ m: "auto", mt: 2 }}
onClick={handleExpand}
>
{show ? <RemoveIcon /> : <AddIcon />}
</Fab>
) : null}
</Grid>
</CardContent>
</Card>
{/* LOG MOVIE PANEL */}
{details.userData === undefined ? (
<UnwatchedPanel
date={userData.date}
onDateChange={handleDate}
rating={userData.rating}
onRatingChange={handleRating}
review={userData.review}
onReviewChange={handleReview}
view_count={
details.userData === undefined ? null : details.userData.view_count
}
onClick={onSubmit}
/>
) : (
<WatchedPanel
date={userData.date}
onDateChange={handleDate}
rating={details.userData.rating}
review={details.userData.review}
view_count={details.userData.view_count}
onSubmit={onSubmit}
onSaveChanges={handleUpdateDetails}
/>
)}
</Container>
);
}
export default Details;
My mistake, actually: this is, indeed, the normal React behavior. If you're navigating from one (scrollable) component to another the scrollbar isn't aware of it, since you're just changing views without reloading the whole page, and will keep it's y (and x) position. To avoid this behavior you need to manually set the scroll position on re-render, like so:
useEffect(() => {
window.scrollTo({ top: 0, left: 0 });
// Do some stuff
}, []);

Uncaught TypeError: Cannot read properties of undefined (reading 'source')

I'm trying to access data from commerceJS API fetched on a different module and passed down as a prop, the code only breaks when I call a nested product object with this error Uncaught TypeError: Cannot read properties of undefined (reading 'source').
I have used the same API on the storefront and {product.media.source} works fine. I'm trying to make a preview module and when I call the nested object it breaks.
Please help.
Here's my code
const Preview = ({ products, onAddToCart }) => {
const [product, setProduct] = useState({});
const params = useParams();
const findProduct = (id) => {
return products.find((product) => {
setProduct(product);
return product.id === id;
});
};
const productId = params.id;
useEffect(() => {
findProduct(productId);
});
return (
<Card sx={{ maxWidth: 345 }} style={{ marginTop: '90px' }}>
<CardMedia image={product.media.source} title={product.name}></CardMedia>
<CardContent>
<Typography gutterBottom variant='h5' component='div'>
{product.name}
</Typography>
<Typography
variant='body2'
color='textSecondary'
dangerouslySetInnerHTML={{ __html: product.description }}
></Typography>
</CardContent>
<CardActions>
<IconButton component={Link} variant='outlined' to='/'>
Go Back
</IconButton>
<IconButton aria-label='Add to Cart' onClick={() => onAddToCart(product.id, 1)}>
<AddShoppingCart />
</IconButton>
</CardActions>
</Card>
);
Here's the product Object
product obj
Apparently your product object does not contain media field by default. You need to wait until it is available before using it.
Try something like this:
const Preview = ({ products, onAddToCart }) => {
const [product, setProduct] = useState();
const params = useParams();
console.log(products[0]);
console.log(params.id);
const findProduct = useCallback((id) => {
return products.find((product) => {
if (product.id === id) { // <-- render optimization
setProduct(product);
return true;
}
return false;
});
}, [products]);
const productId = useMemo(() => params.id, [params.id]);
useEffect(() => {
findProduct(productId);
}, [productId, findProduct]);
const renderCard = useCallback(() => {
if (!product) return null; // <-- here you can implement your placeholder component
return (
<Card sx={{ maxWidth: 345 }} style={{ marginTop: '90px' }}>
<CardMedia image={product.media.source} title={product.name}/>
<CardContent>
<Typography gutterBottom variant='h5' component='div'>
{product.name}
</Typography>
<Typography
variant='body2'
color='textSecondary'
dangerouslySetInnerHTML={{ __html: product.description }}
></Typography>
</CardContent>
<CardActions>
<IconButton component={Link} variant='outlined' to='/'>
Go Back
</IconButton>
<IconButton aria-label='Add to Cart' onClick={() => onAddToCart(product.id, 1)}>
<AddShoppingCart />
</IconButton>
</CardActions>
</Card>
);
}, [product]);
return renderCard();
};
Or like this:
const initialState = { // <-- you need to change this as you need
active: true,
assets: [/* ... */],
categories: [/* ... */],
checkout_url: {
checkout: 'placeholder://checkout.url',
display: 'placeholder://display.url',
/* ... */
},
collects: {/*...*/}
created: 0,
description: 'placeholder description',
extra_fields: [/* ... */],
hsa: {/* ... */},
id: 'placeholder_id',
invenotry: {/* ... */},
is: {/* ... */},
media: {
type: 'image',
source: 'placeholder://image.source',
/* ... */
},
meta: null,
/* ... */
};
const Preview = ({ products, onAddToCart }) => {
const [product, setProduct] = useState(initialState);
const params = useParams();
console.log(products[0]);
console.log(params.id);
const findProduct = useCallback((id) => {
return products.find((product) => {
if (product.id === id) { // <-- render optimization
setProduct(product);
return true;
}
return false;
});
}, [products]);
const productId = useMemo(() => params.id, [params.id]);
useEffect(() => {
findProduct(productId);
}, [productId, findProduct]);
return (
<Card sx={{ maxWidth: 345 }} style={{ marginTop: '90px' }}>
<CardMedia image={product.media.source} title={product.name}/>
<CardContent>
<Typography gutterBottom variant='h5' component='div'>
{product.name}
</Typography>
<Typography
variant='body2'
color='textSecondary'
dangerouslySetInnerHTML={{ __html: product.description }}
></Typography>
</CardContent>
<CardActions>
<IconButton component={Link} variant='outlined' to='/'>
Go Back
</IconButton>
<IconButton aria-label='Add to Cart' onClick={() => onAddToCart(product.id, 1)}>
<AddShoppingCart />
</IconButton>
</CardActions>
</Card>
);
};
Based on your code above
console.log(products[0]);
console.log(params.id);
const findProduct = (id) => {
return products.find((product) => {
setProduct(product);
return product.id === id;
});
};
Looks like the product is an array and not an object. I think this is where you might have missed.
Hence, product.media.source throws error as product.media is undefined.
Please check which product you might want to add.

CSS Changes not reflected on associated state change in React with React Hooks

I want to make a section where some of the sections get disabled when a particular button is clicked, the way I am doing it is by creating two different CSS properties like so:
export const useStyles = makeStyles((theme: Theme) =>
createStyles({
disabledWithSpacing: {
padding: theme.spacing(2),
pointerEvents: "none",
opacity: 0.15,
},
bottomPadded: {
paddingBottom: theme.spacing(2),
},
})
);
My component is like this:
export function SingleEmailCampaign() {
const [disabled, setDisabled] = React.useState<disabledSections>({
to: false,
from_email: false,
subject: false,
content: false,
});
const classes = useStyles();
const sections = ["to", "from_email", "subject", "content"];
const disableSections = (sectionName: String) => {
var new_mapping: disabledSections = disabled;
for (let section of sections) {
new_mapping[section] = section === sectionName ? false : true;
}
setDisabled(new_mapping);
console.log(disabled);
};
return (
<Container maxWidth="lg" className={classes.container}>
<AppBar position="static" className={classes.transparentAppBar}>
<Toolbar className={classes.containerTopPadded}>
<Typography className={classes.title} variant="h4" noWrap>
Let's get started
</Typography>
<div className={classes.appBarEnd}>
<Button
color="primary"
variant="outlined"
className={classes.button}
>
Schedule
</Button>
<Button
color="primary"
variant="outlined"
className={classes.button}
>
Send
</Button>
</div>
</Toolbar>
</AppBar>
<Grid container spacing={6} className={classes.containerTopPadded}>
<Grid item xs={12} lg={12}>
<Typography component="h1" variant="h4">
{campaign?.name}
</Typography>
<Button>Edit Name</Button>
<Box
border={1}
className={
disabled.to ? classes.disabledWithSpacing : classes.spacing
}
>
Sample To
</Box>
<Box
border={1}
className={
disabled.from_email
? classes.disabledWithSpacing
: classes.spacing
}
>
Sample From
</Box>
<Box
border={1}
className={
disabled.subject ? classes.disabledWithSpacing : classes.spacing
}
>
</Box>
Sample Subject
<Box
border={1}
className={
disabled.content ? classes.disabledWithSpacing : classes.spacing
}
>
Sample Content
</Box>
</Grid>
</Grid>
</Container>
);
}
My expectation is that the sections other than the one clicked should get disabled, the state is being updated correctly but the sections are not getting disabled. Any suggestions would really help.
The issue is the way you are trying to copy the state. You cannot/shouldn't mutate the current state. When you use:
var new_mapping: disabledSections = disabled;
You are mutating the original state. What you need to do is perform a shallow merge. I would just use the spread operator on it. Change
var new_mapping: disabledSections = disabled;
should be
var new_mapping: disabledSections = {...disabled}
Example: (not typescript)
const disableSections = (value) => {
let new_mapping = { ...disabled };
for (let section of sections) {
new_mapping[section] = section === value ? false : true;
}
setDisabled(new_mapping);
};
sandbox link: https://codesandbox.io/s/bold-sunset-7kn0w?file=/src/App.js

Resources