React map always calls method with data of last element - reactjs

I am making a social media app. I am looping through all the comments and showing it on UI. When I click on edit, always the last comment's text show up on input. I tried many different things, changed structure, used bind to bind the context, but nothing has helped.
I am using React Material UI.
Here is the code:
render() {
const { anchorEl } = this.state;
const open = Boolean(anchorEl);
return(
<Panel>
<form noValidate autoComplete="off" onSubmit={this.onSubmit}>
<TextField
id={`comment${this.state.post_id}`}
name="comment"
fullWidth
placeholder="Comment here"
margin="normal"
value={this.state.comment}
onChange={this.handleChange}
InputProps={{
endAdornment : <InputAdornment position="end">
<IconButton onClick={this.resetTextField}>
<CloseIcon/>
</IconButton>
</InputAdornment>
}}
/>
</form>
{this.props.comments && this.props.comments.length > 0 &&
<RenderComments
comments={this.state.comments}
open={open}
anchorEl={this.state.anchorEl}
handleClick={this.handleClick}
handleClose={this.handleClose}
handleDelete={this.handleDelete}
handleEdit={this.handleEdit}
/>
}
</Panel>
)
}
const RenderComments = (props) => {
return props.comments.map(comment =>
<CommentBase
key={comment.id}
comment={comment}
open={props.open}
anchorEl={props.anchorEl}
handleClick={props.handleClick}
handleClose={props.handleClose}
handleDelete={props.handleDelete}
handleEdit={props.handleEdit}
/>
);
};
const CommentBase = ({comment, open, anchorEl, handleClick, handleClose, handleDelete, handleEdit}) => (
<CommentBasePanel>
<CommentText>
{comment.text}
<span style={{ float: 'right', fontSize: 10, color: '#A9A9A9' }}>{moment(comment.created_at).fromNow()}</span>
</CommentText>
<HelperAction>
<MoreVertIcon
id={comment.id}
aria-owns={open ? `simple-popper${comment.id}` : null}
aria-haspopup="true"
variant="contained"
onClick={handleClick}
/>
<Popover
id={`simple-popper${comment.id}`}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<Typography style={{ padding: 10, cursor: 'pointer' }} onClick={() => handleEdit(comment)}>
Edit
</Typography>
<Typography style={{ padding: 10, color: red[500], cursor: 'pointer' }} onClick={() => handleDelete(comment.id)}>
Delete
</Typography>
</Popover>
</HelperAction>
</CommentBasePanel>
);
handleEdit = (comment) => {
this.setState({ comment: comment.text, comment_id: comment.id })
};
A console log on comment here in handleEdit method always logs the last comment no matter what comment I edit. Edit on first comment gives last comment text as you can see in the image.

Bad Popovers management
map copies the same open and anchorEl props to all Popover instances - handleClick (you didn't show this) *opens all of them in the same place** (the last one is on top).
FIX: include id in handleClick, save in state and use in condition for open property
FIX2: You can use one <Popover/> instance especially when not displaying any content related to specific comment.
PS. I spent more time recreating this (guessing missing parts) in stackblitz than real debugging (in fact only checking html structure for <Popover/> with rendered {comment.id}). Next time show more complete code or provide minimal working example.

If you update your RenderComments render method as follows, it should resolve your problem:
const RenderComments = (props) => {
return props.comments.map(comment =>
<CommentBase
key={comment.id}
comment={comment}
open={props.open}
anchorEl={props.anchorEl}
handleClick={ props.handleClick }
handleClose={ props.handleClose }
handleDelete={ () => props.handleDelete(comment.id) }
handleEdit={ () => props.handleEdit(comment) }
/>
);
};

Related

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

Factoring material UI popover into a higher order component and keep the popover state in the `hoc`

I am following the react material ui doc here: https://mui.com/components/popover/, and I'm attempting to factor all the popover view and state logic into a higher order component. The one from the doc is here:
function BasicPopoverFromDoc(props) {
const [anchorEl, setAnchorEl] = React.useState(null);
const handlePopoverOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handlePopoverClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
return (
<div>
<Typography
aria-owns={open ? 'mouse-over-popover' : undefined}
aria-haspopup="true"
onMouseEnter={handlePopoverOpen}
onMouseLeave={handlePopoverClose}
style={{color:'white'}}
>
Hover with a Popover.
</Typography>
<Popover
id="mouse-over-popover"
sx={{
pointerEvents: 'none',
}}
open={open}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={handlePopoverClose}
disableRestoreFocus
>
<Typography sx={{ p: 1 }}>I use Popover.</Typography>
</Popover>
</div>
);
}
I lifted all the state logic into a hoc as follows, taking <Child/> as a param:
function WithPopOver(props){
const [anchorEl, setAnchorEl] = React.useState(null);
const handlePopoverOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handlePopoverClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const { Child } = props;
console.log('with Popover: ', open)
return (
<div>
<Child
{...props}
aria-owns={open ? 'mouse-over-popover' : undefined}
aria-haspopup="true"
onMouseEnter={handlePopoverOpen}
onMouseLeave={handlePopoverClose}
/>
<Popover
id="mouse-over-popover"
sx={{
pointerEvents: 'none',
}}
open={open}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={handlePopoverClose}
disableRestoreFocus
>
<Typography sx={{ p: 1 }}>I use Popover.</Typography>
</Popover>
</div>
);
}
but when i try to use it this way:
function BasicPopover(props){
return (
<WithPopOver
{...props}
Child={() => <Typography style={{color:'white'}}> wrapped in hoc </Typography>}
/>
)
}
It seems like the popover does not display. It refuses to display even when I change open to true by force. What am i missing here?
You haven't passed Child props to it's children, so onMouseEnter and onMouseLeave never fires, try on this:
<WithPopOver
{...props}
Child={(props) => <Typography style={{color:'white'}} {...props}> wrapped in hoc </Typography>}
/>

history.push on form submit

I'm a newbie to react and I'm trying to route on a different set of components after a form (in my case a search bar) has been submitted (using onSubmit event). I have my search component wrapped in withRouter and have the history deconstructed. I've tried using onClick and the push does trigger, but does not seem to do anything with onSubmit. Hoping for your advise.
Thank you
Search component
const SearchAppBar = (props) => {
const {
history,
onPlatformDrop,
onPlatformChange,
onPriceRangeDrop,
clearPriceRange,
searchQuery,
setSearchQuery,
clearGenre,
onDropDownChange,
} = props;
return (
<Box sx={{ flexGrow: 1 }}>
<Search
sx={{
width: { xs: "85vw", md: "50vw", lg: "30vw" },
margin: "auto",
marginBottom: "20px",
}}
>
<form action="/" method="get" style={{ display: "flex" }}>
<StyledInputBase
defaultValue={searchQuery}
autoComplete="off"
placeholder="Search All Games…"
inputProps={{ "aria-label": "search" }}
type="search"
name="s"
id="site-search"
/>
<SearchIconWrapper>
<IconButton
type="submit"
onSubmit={(e) => {
history.push("/find");
clearGenre();
clearPriceRange();
onPlatformChange("");
onPlatformDrop("All Platforms");
onPriceRangeDrop("All Price Range");
onDropDownChange("All Genres");
setSearchQuery(e.target.value);
}}
>
<SearchIcon style={{ color: "#55597d" }} />
</IconButton>
</SearchIconWrapper>
</form>
</Search>
</Box>
);
};
export default withRouter(SearchAppBar);
Then on my App, i have something like this:
<Route
path="/find"
exact
render={(props) => (
<div>
<Search
onPlatformChange={onPlatformChange}
onPlatformDrop={onPlatformDrop}
clearPriceRange={clearPriceRange}
onPriceRangeDrop={onPriceRangeDrop}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
clearGenre={clearGenre}
onDropDownChange={onDropDownChange}
/>
</div>
)}
/>;
The onSubmit handler should be on the form element, not the button submitting the form. Remove the form action and method. You will also need to prevent the default form action from occurring, if you don't then the form is submitted and the page reloads.
<form
style={{ display: "flex" }}
onSubmit={(e) => { // handles form submission
e.preventDefault(); // prevents default form action
history.push("/find"); // navigate
...
setSearchQuery(e.target.s.value); // <-- access field value
}}
>
<StyledInputBase
...
name="s" // <-- access in submit handler
...
/>
<SearchIconWrapper>
<IconButton type="submit">
<SearchIcon style={{ color: "#55597d" }} />
</IconButton>
</SearchIconWrapper>
</form>

React Display Separate Popover Component on Menu Click

I have the below menu:
So, ideally you'd click on those 3 dots which would open up another box next to it, I decided to use a popover(https://material-ui.com/components/popover/).. The problem is that when you click the 3 dots nothing happens. I assume this is because onClick function returns a functional popover component but that doesn't get displayed. I put in debuggers and alerts inside the functional component no hit at all.
This is those 3 dots
<IconButton
aria-describedby="simple-popover"
variant="contained"
onClick={e => this.moreClick(e, props.children)}
>
<More />
</IconButton>
This is moreClick function
moreClick = (e, officeAccount) => {
return (
<AccountFavoritesPopover element={e} officeAccount={officeAccount} />
);
};
This is the whole popover
import React from "react";
import Popover from "#material-ui/core/Popover";
export default function AccountFavoritesPopover(element, officeAccount) {
const anchorEl = element;
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
return (
<div>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
//onClose={alert}
anchorOrigin={{
vertical: "top",
horizontal: "right"
}}
transformOrigin={{
vertical: "top",
horizontal: "left"
}}
>
<div>{officeAccount}</div>
</Popover>
</div>
);
}
Does popover have to be inside the main file? Currently the 3 dots is in the main file and AccountFavoritesPopover is a whole separate file.
I attempted to put "AccountFavoritesPopover" code inside the main file but I cannot utilize useState in the main file because it's a class. Also, I cannot convert it to actual state and use setState because once setState kicks in, the menu will disappear...
Edit:
Below is the Menu
<Creatable
id="Select"
menuIsOpen={this.state.Focused}
components={{
Option: this.Option
}}
/>
Below is the Options inside menu
<div style={{ position: "relative" }}>
<components.Option {...props} />
<div id="MoreBox">
<IconButton
aria-describedby="simple-popover"
variant="contained"
onClick={e => this.moreClick(e, props.children)}
>
<More />
</IconButton>
</div>
</div>
Try this, this should work(Not tested)
Main.js
export default class Main extends Component {
constructor(props) {
this.state = {
selectedIndex: 0,
selectedId: 0,
anchorEl: null
};
}
moreClick = (anchorEl, selectedId, selectedIndex) => {
this.setState({
selectedId,
selectedIndex,
anchorEl,
open: true,
});
}
handleClose = () => {
this.setState({
open: false
});
}
render() {
const menu = [
{
id: 1,
text: '002',
more: 'more 003'
},
{
id: 2,
text: '003',
more: 'more 003'
},
{
id: 3,
text: '004',
more: 'more 003'
}
]
const menuDom = menu.map((m, index) => {
return (
<IconButton
key={m.id}
aria-describedby="simple-popover"
variant="contained"
onClick={e => this.moreClick(e.currentTarget, index, m.id)}>
{m.text}
</IconButton>
)
})
const more = (<More>{menu[this.state.selectedIndex].text}</More>)
return (
<div>
{menuDom}
<AccountFavoritesPopover open={this.state.open} anchorEl={this.state.anchorEl} onClose={this.handleClose}>
{more}
</AccountFavoritesPopover>
</div>
)
}
}
AccountFavoritesPopover.js
export default function AccountFavoritesPopover(open, anchorEl, onClose) {
const id = open ? "simple-popover" : undefined;
return (
<div>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{
vertical: "top",
horizontal: "right"
}}
transformOrigin={{
vertical: "top",
horizontal: "left"
}}
>
<div>{this.props.children}</div>
</Popover>
</div>
);
}

Material ui anchor without hooks

I have a menu component that pops open in a table. When I copy the material ui example into the cell in the table it works perfectly.
https://codesandbox.io/s/gitl9
The material ui example uses hooks and I want to change it to a class component and use redux.
When I made the change the pop-up menu does not align beside the the button you press anymore.
The anchorEl attribute is responsible for passing the location of the button that has been called.
I added these attributes allow me to move the menu pop but it does not align with button that you click to open the menu.
const options = ["View", "Edit", "Delete"];
const ITEM_HEIGHT = 48;
class ActionsOptionMenu extends Component {
state = { anchorEl: null };
render() {
return (
<div>
<IconButton
aria-label='more'
aria-controls='long-menu'
aria-haspopup='true'
>
<MoreVertIcon />
</IconButton>
<Menu
getContentAnchorEl={null}
anchorOrigin={{
height: "54px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: `0 ${padding} 0 ${padding}`,
margin: "0 auto 7px auto"
}}
transformOrigin={{ vertical: "top", horizontal: "right" }}
id='long-menu'
anchorEl={anchorEl}
keepMounted
open={true}
PaperProps={{
style: {
maxHeight: ITEM_HEIGHT * 4.5,
width: 120
}
}}
>
{options.map(option => (
<MenuItem key={option}>{option}</MenuItem>
))}
</Menu>
</div>
);
}
}
I solved the issue doing it this way.
render() {
const { open } = this.state;
return (
<div>
<IconButton
aria-label='more'
aria-controls='long-menu'
aria-haspopup='true'
buttonRef={node => {
this.anchorEl = node;
}}
onClick={event => this.handleClick(event)}
>
<MoreVertIcon />
</IconButton>
<Popper
open={open}
anchorEl={this.anchorEl}
transition
disablePortal
style={{ zIndex: 100 }}
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
id='menu-list-grow'
style={{
zIndex: 1001,
transformOrigin:
placement === "bottom" ? "center top" : "center bottom"
}}
>
<Paper>
<ClickAwayListener
onClickAway={event => this.handleClose(event)}
>
<MenuList>
<MenuItem onClick={event => this.handleClose(event)}>
Profile
</MenuItem>
<MenuItem onClick={event => this.handleClose(event)}>
My account
</MenuItem>
<MenuItem onClick={event => this.handleClose(event)}>
Logout
</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
);
}

Resources