how to prevent duplicate onChange values within map loop - reactjs

I'm trying to prevent onChange values from duplicating within each item.
for example
onChange method for comments
handleCommentChange = (e) => {
this.setState({
comment_body: e.target.value
})
}
I'm assuming i would have to loop through keys or something within the onChange method. I'm not sure how i would go about doing this.
something like
Pseudo Attempt
Object.keys(this.state.items).forEach(key){
this.setState({
comment_body: e.target.value[key]
})
}
Or is there a better way ? not sure.
map iteration code
{this.state.images.length > 0 ? (
this.state.images.map( (img, i) => (
<Grid item sm={12} md={12} key={img.id} style={{ margin: '30px 0px'}}>
<Paper style={{padding:'20px 20px'}}>
{/* // empty image_title */}
<Typography style={{ padding: '30px 5px', letterSpacing:'8px', textTransform:'uppercase'}} variant="h4" align="center">{img.image_title}</Typography>
<Divider style={{ width: '150px', margin:'10px auto', backgroundColor:'#000000'}} variant="middle" />
<Image image_url={img.img_url} />
<Typography variant="h6" align="center">{img.user.username}</Typography>
<Typography variant="h6" align="center">{moment(img.created_at).calendar()}</Typography>
<Button onClick ={() => this.writeComment(img.id)} variant="outlined" component="span" color="primary">
{this.state.isComment === img.id ? "Close" : "Write A Comment"}
</Button>
{/* here were prevent comments being selected for all items in the array, renders the comment form you clicked on. */}
{this.state.isComment === img.id ?
<Comment onSubmit={this.commentSubmit}
commentBody={this.state.comment_body }
commentChange={this.handleCommentChange}/>
: null}
{/* hide delete button when user enters comment */}
{!this.state.isComment ? <Button style={{margin: '0px 20px'}} onClick={() => this.deleteImg(img.id)} variant="outlined" component="span" color="primary">
Delete
</Button> : null}
</Paper>
</Grid>
))
) : (
<div>
<Grid item md={8}>
<Typography>No Images yet</Typography>
</Grid>
</div>
)}

The problem is that all your comments refer to the same value in your component-state. I wanted to touch upon this in your earlier question.
What you should be doing is separating your Grid markup into its own component like this:
ImageContainer.js
import React, { Component } from "react";
import Button from '#material-ui/core/Button';
import TextField from '#material-ui/core/TextField';
import Grid from '#material-ui/core/Grid';
import Typography from '#material-ui/core/Typography';
import Paper from '#material-ui/core/Paper';
import Divider from '#material-ui/core/Divider';
import Image from './Image';
import moment from 'moment';
import Comment from './Comment';
class ImageContainer extends React.Component{
state = {
isComment: false,
comment_body: ""
}
handleCommentChange = (e) => {
this.setState({
comment_body: e.target.value
})
}
writeComment = (id) => {
this.setState({
isComment: this.state.isComment ? '' : id // check if you state is filled to toggle on/off comment
})
}
commentSubmit = (e) => {
e.preventDefault();
console.log(this.state.comment_body);
// Axios.post('/images/newComment', this.state.comment_body).then( (response )=> {
// const newComment = { ...response.data};
// console.log(newComment);
// this.setState({
// comment_body: ''
// })
// })
}
render(){
const { img } = this.props
return(
<Grid item sm={12} md={12} key={img.id} style={{ margin: '30px 0px'}}>
<Paper style={{padding:'20px 20px'}}>
{/* // empty image_title */}
<Typography style={{ padding: '30px 5px', letterSpacing:'8px', textTransform:'uppercase'}} variant="h4" align="center">{img.image_title}</Typography>
<Divider style={{ width: '150px', margin:'10px auto', backgroundColor:'#000000'}} variant="middle" />
<Image image_url={img.img_url} />
<Typography variant="h6" align="center">{img.user.username}</Typography>
<Typography variant="h6" align="center">{moment(img.created_at).calendar()}</Typography>
<Button onClick ={() => this.writeComment(img.id)} variant="outlined" component="span" color="primary">
{this.state.isComment === img.id ? "Close" : "Write A Comment"}
</Button>
{/* here were prevent comments being selected for all items in the array, renders the comment form you clicked on. */}
{this.state.isComment === img.id ?
<Comment onSubmit={this.commentSubmit}
commentBody={this.state.comment_body }
commentChange={this.handleCommentChange}/>
: null}
{/* hide delete button when user enters comment */}
{!this.state.isComment ? <Button style={{margin: '0px 20px'}} onClick={() => this.deleteImg(img.id)} variant="outlined" component="span" color="primary">
Delete
</Button> : null}
</Paper>
</Grid>
)
}
}
export default ImageContainer
Now each ImageContainer has its own state-values to keep track of and update. So there is no duplicates.
Then in your Dashboard component, simply import ImageContainer.
import ImageContainer from "./ImageContainer"
Now for each image in our .map(), we will create a unique instance of that component, and pass the image as a prop to ImageContainer.
{this.state.images.length > 0 ? (
this.state.images.map( (img, i) => (
<ImageContainer img={img}/>
))
) : (
<div>
<Grid item md={8}>
<Typography>No Images yet</Typography>
</Grid>
</div>
)}
Also remember to hook up your event-handlers to your Comment component. At the minimum it needs to use the props you passed in.
Comment.js
import React from "react";
const Comment = props => {
return (
<form onSubmit={props.onSubmit}>
<input
onChange={props.commentChange}
value={props.commentBody}
/>
</form>
);
};
export default Comment;
Here's a sandbox too, I dont have your Comment component code so it wont have the same styling: https://codesandbox.io/s/young-pine-zmpvr

Related

I want to click anywhere on the box to navigate me to the post detail page except the buttons. It's buttons should it's own job

import {
Box,
Flex,
Text,
Input,
Image,
useColorModeValue,
useClipboard,
Divider,
IconButton,
HStack,
Menu,
MenuButton,
MenuList,
Portal,
MenuItem,
useToast,
useOutsideClick,
useDisclosure,
} from "#chakra-ui/react";
import {
BsThreeDots,
BsDot,
BsBookmark,
BsFlag,
BsCheckCircle,
} from "react-icons/bs";
import { BsHeart } from "react-icons/bs";
import { AiFillHeart, AiFillMessage } from "react-icons/ai";
import { FaShareSquare } from "react-icons/fa";
import parse from "html-react-parser";
import copyLink from "#/images/copy-link.svg";
import { axiosInstance } from "#/axiosConfig";
import { COLORS } from "../../constants/COLORS";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { useDispatch, useSelector } from "react-redux";
import { Link, useNavigate } from "react-router-dom";
import { addNotice, deleteMyPostSlice } from "../../redux/slices/userSlice";
import { FiEdit } from "react-icons/fi";
import { HiOutlineTrash } from "react-icons/hi";
import {
addComment,
deletePost,
publishPost,
unPublishPost,
} from "../../redux/asyncActions/postAction";
import CustomImage from "../small/CustomImage";
import {
deletePostSlice,
removeBookmarkSlice,
} from "../../redux/slices/postSlice";
import {
EmailShareButton,
FacebookShareButton,
TwitterShareButton,
LinkedinShareButton,
FacebookIcon,
TwitterIcon,
LinkedinIcon,
EmailIcon,
} from "react-share";
import AddPicker from "../small/AddPicker";
import PostTag from "./PostTag";
import LikedPeople from "../small/LikedPeopleModal";
import { useRef } from "react";
import ReportModal from "../common/ReportModal";
dayjs.extend(relativeTime);
var domain = window.location.host;
const NewsFeedPost = ({ data }) => {
const [liked, setLiked] = useState(data?.has_liked);
const [hasFav, setHasFav] = useState(data?.has_favorited);
const [likedCount, setLikedCount] = useState(data?.likers_count);
const [commentInput, setCommentInput] = useState("");
const { isOpen, onOpen, onClose } = useDisclosure();
const userId = useSelector((state) => state.userReducer?.userInfo?.id);
const { hasCopied, onCopy } = useClipboard(
`${domain}/post-detail/${data?.id}`
);
const toast = useToast();
const bg = useColorModeValue("white", COLORS.darkGray);
const borderColor = useColorModeValue(
COLORS.lightGrayBorder,
COLORS.darkGrayBorder
);
const navigate = useNavigate();
const ref = useRef();
const [isModalOpen, setIsModalOpen] = useState(false);
useOutsideClick({
ref: ref,
handler: () => setIsModalOpen(false),
});
const dispatch = useDispatch();
const likeThisPost = async (post_id) => {
try {
const r = await axiosInstance.post(`post/like/${post_id}`);
setLikedCount(r.data.likers_count);
setLiked((prev) => !prev);
} catch (err) {}
};
const bookmarkThisPost = async (post_id) => {
try {
const r = await axiosInstance.post(`post/favorite/${post_id}`);
setHasFav(r.data.favorited);
if (r.data.favorited) {
dispatch(addNotice("This post has been bookmarked"));
} else {
dispatch(addNotice("Bookmark has been removed"));
}
dispatch(removeBookmarkSlice(post_id));
} catch (err) {}
};
const deleteThisPost = (id) => {
dispatch(deletePost(id));
dispatch(deleteMyPostSlice(id));
};
const addMyComment = (e, id) => {
e.preventDefault();
let data = {
post_id: id,
content: commentInput,
};
dispatch(addComment(data));
navigate(`/post-detail/${id}`, { replace: true });
};
const unPublishPosts = (id) => {
dispatch(unPublishPost(id));
dispatch(deletePostSlice(id));
};
const publishPosts = (id) => {
dispatch(publishPost(id));
dispatch(deleteMyPostSlice(id));
};
return (
<Box
bg={bg}
mt="1.5rem"
p="1.5rem"
pt="0.8rem"
borderWidth={1}
borderColor={borderColor}
style={{ borderRadius: "10px" }}
onClick={() => navigate(`/post-detail/${data?.id}`)}
>
<Flex mb="1.5rem" align="center" direction="row" justify="space-between">
<Link to={`/post-detail/${data?.id}`}>
<Text fontWeight="400">{data?.title}</Text>
</Link>
<Menu>
<MenuButton>
<BsThreeDots />
</MenuButton>
<Portal>
<MenuList w="100px" p="3" bg={bg}>
{data?.user.id === userId ? (
<Flex direction={"column"} ml="1rem">
<Link to={`/post-edit/${data?.id}`}>
<Flex className="menuitems">
<FiEdit />
<Text ml="4" fontSize="0.8rem">
Edit
</Text>
</Flex>
</Link>
<Flex
className="menuitems"
onClick={() => deleteThisPost(data?.id)}
>
<HiOutlineTrash />
<Text ml="4" fontSize="0.8rem">
Delete
</Text>
</Flex>
{!data.is_published ? (
<Flex
className="menuitems"
onClick={() => publishPosts(data?.id)}
>
<BsCheckCircle />
<Text ml="4" fontSize="0.8rem">
Publish
</Text>
</Flex>
) : (
<Flex
className="menuitems"
onClick={() => unPublishPosts(data?.id)}
>
<FiEdit />
<Text ml="4" fontSize="0.8rem">
Unpublish
</Text>
</Flex>
)}
<Flex
className="menuitems"
onClick={() => bookmarkThisPost(data?.id)}
>
<BsBookmark color={hasFav ? "lightgreen" : ""} />
<Text ml="4" fontSize="0.8rem">
{hasFav ? "Remove Bookmark" : "Bookmark"}
</Text>
</Flex>
</Flex>
) : (
<Flex direction={"column"} ml="1rem">
<Flex
className="menuitems"
onClick={() => bookmarkThisPost(data?.id)}
>
<BsBookmark color={hasFav ? "lightgreen" : ""} />
<Text ml="4" fontSize="0.8rem">
{hasFav ? "Remove Bookmark" : " Bookmark"}
</Text>
</Flex>
<Flex className="menuitems" onClick={onOpen}>
<BsFlag />
<Text ml="4" fontSize="0.8rem">
Report
</Text>
<ReportModal
modalIsOpen={isOpen}
closeModal={onClose}
type="post"
id={data?.id}
/>
</Flex>
</Flex>
)}
</MenuList>
</Portal>
</Menu>
</Flex>
<Flex direction="row" align="center" justify="space-between">
<Box>
<Flex direction="row" align="center">
<Link to={`/user-profile/${data?.user?.id}`}>
<CustomImage
size="50px"
char={data.user?.firstname.charAt(0)}
imageUrl={data?.user?.photo}
alt={`${data.user?.firstname}'s Avatar`}
role={data?.user?.role}
/>
</Link>
<Flex ml="1rem" direction="column">
<Link to={`/user-profile/${data?.user?.id}`}>
<Text
fontSize={["12px", "13px", "13px", "13px"]}
mb="2px"
fontWeight="500"
>
{data?.user?.firstname} {data?.user?.lastname}
</Text>
</Link>
<Flex direction="row" align="center">
<Text
style={{ color: "#ABAAAF" }}
fontSize={["9px", "9px", "9px", "12px"]}
>
{dayjs(data.created_at).fromNow()}
</Text>
<BsDot
color="#ABAAAF"
fontSize={"20px"}
// fontSize={["14px","14px","15px","24px"]}
ml="4px"
/>
<Flex fontSize="10px" ml="4px">
in{" "}
<Text
ml="1"
cursor="pointer"
color="#FF8053"
onClick={() =>
navigate(`/?category_id=${data?.category?.id}`)
}
>
{data?.category?.name}
</Text>
</Flex>
</Flex>
</Flex>
</Flex>
</Box>
{data.tags && <PostTag tags={data?.tags} />}
</Flex>
<Box className="parseContent" mt="1.5rem" fontSize="12px">
<Link to={`/post-detail/${data?.id}`}>{parse(data.content_html)}</Link>
</Box>
<Divider mt="1rem" mb="0.5rem" />
<Flex
direction="row"
justify={{ base: "start", sm: "space-between" }}
mt="1rem"
align="center"
>
<Box w={{ base: "50%", sm: "50%", md: "70%", lg: "70%" }}>
<Box pos="relative">
<form
onSubmit={(e) => addMyComment(e, data?.id)}
style={{ position: "relative" }}
>
<Input
value={commentInput}
onChange={(e) => setCommentInput(e.target.value)}
fontSize={["9px", "10px", "12px", "12px"]}
placeholder="Add Response..."
// zIndex={0}
/>
<AddPicker setInput={setCommentInput} />
</form>
</Box>
</Box>
<Box width={{ base: "45%", sm: "45%", lg: "40%" }} pl="1.5rem">
<Flex direction="row" justify="space-around" position="relative">
<Flex direction="row" align="center">
<IconButton
onClick={() => likeThisPost(data.id)}
_hover={{ background: "transparent" }}
_active={{ background: "transparent" }}
_focus={{ boxShadow: "none" }}
bg="transparent"
color="#C5D0E6"
children={
liked ? (
<AiFillHeart fontSize={"1.7rem"} color={COLORS.hasLiked} />
) : (
<BsHeart fontSize={"1.4rem"} />
)
}
/>
<Text
color="#ABAAAF"
fontSize="14px"
onClick={() => setIsModalOpen(true)}
cursor="pointer"
>
{likedCount}
</Text>
{isModalOpen && (
<Box ref={ref}>
<LikedPeople />
</Box>
)}
</Flex>
<Link to={`/post-detail/${data?.id}`}>
<DataIconCount icon="AiFillMessage" count={data.comments_count} />
</Link>
<Menu>
<MenuButton
bg="transparent"
color="#C5D0E6"
_hover={{ background: "transparent", color: "#7B6CB4" }}
_active={{ background: "transparent" }}
_focus={{ boxShadow: "none" }}
className="newsfeed-iconbtn"
as={IconButton}
aria-label="Options"
icon={<FaShareSquare fontSize={"1.5rem"} />}
/>
<MenuList bg={bg}>
<MenuItem fontSize="12px">
<HStack align="center" gap="4px">
<FacebookShareButton
url={`${domain}/post-detail/${data?.id}`}
>
<Flex>
<FacebookIcon size={22} />
<Text ml="3">Facebook</Text>{" "}
</Flex>
</FacebookShareButton>
</HStack>
</MenuItem>
<MenuItem fontSize="12px">
<TwitterShareButton url={`${domain}/post-detail/${data?.id}`}>
<Flex>
<TwitterIcon size={22} />
<Text ml="3">Twitter</Text>{" "}
</Flex>
</TwitterShareButton>
</MenuItem>
<MenuItem fontSize="12px">
<LinkedinShareButton
url={`${domain}/post-detail/${data?.id}`}
>
<Flex>
<LinkedinIcon size={22} />
<Text ml="3">LinkedIn</Text>{" "}
</Flex>
</LinkedinShareButton>
</MenuItem>
<MenuItem fontSize="12px">
<EmailShareButton url={`${domain}/post-detail/${data?.id}`}>
<Flex>
<EmailIcon size={22} />
<Text ml="3">Mail</Text>
</Flex>
</EmailShareButton>
</MenuItem>
<MenuItem fontSize="12px">
<HStack
onClick={() => {
onCopy();
toast({
description: "Copied Post Link",
status: "success",
position: "top-right",
});
}}
align="center"
gap="4px"
>
<Image src={copyLink} />
<Text>Copy Link</Text>
</HStack>
</MenuItem>
</MenuList>
</Menu>
</Flex>
</Box>
</Flex>
</Box>
);
};
I
I have a post where there are lots of button. Each button has it's own task. Whenever I click on any white space or anywhere except this buttons, I should get navigated to post details page. So I have assigned navigate function on the onclick of the parent container that is "Box". The issue is that since all children are wrapped in box so clicking on those buttons also navigate . I want to navigate only on white spaces. I wrote a clumsy way of using e.stopPropagation() so buttons click prevent navigate() but still few buttons are navigating. Is there any way of navigating only when anywhere except buttons?
In the click event listener, check if the event current target is not a button.
if(!evt.currentTarget.matches('button'))

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

How to access child ref from parent without fowardRef

I have a ref which works on the parent component, but i need this ref on the child component.
const divRef = React.useRef<any>();
However props.ref is showing undefined on the commentListContainer. What should i pass on the commentListContainer ?
I saw that forwardRef could be used in a situation like this, but im unsure how this fowardRef would work using hooks in a typescript manner.
PostItemContainer.tsx
import React, { Fragment, useState, useRef } from "react";
import Avatar from "#material-ui/core/Avatar";
import Button from "#material-ui/core/Button";
import Grid from "#material-ui/core/Grid";
import Paper from "#material-ui/core/Paper";
import Typography from "#material-ui/core/Typography";
import DeleteOutlineOutlinedIcon from "#material-ui/icons/DeleteOutlineOutlined";
import FavoriteIcon from "#material-ui/icons/Favorite";
import FavoriteBorderIcon from "#material-ui/icons/FavoriteBorder";
import moment from "moment";
import { toast, ToastContainer } from "react-toastify";
import OurLink from "../../../common/OurLink";
import CommentForm from "../comment/CommentForm";
import CommentList from "../commentList/CommentList";
import OurModal from "../../../common/OurModal";
import "react-toastify/dist/ReactToastify.css";
function PostItemContainer(props: any) {
const [openModal, setOpenModal] = useState(false);
const [openForm, setOpenForm] = useState(false);
const [comment_body, setCommentBody] = useState("");
const [gifUrl, setGifUrl] = useState("");
const divRef = React.useRef<any>();
const writeComment = () => {
// this is the same as this.setState({ openForm: !this.state.open })
setOpenForm(!openForm);
};
const commentChange = (comment) => {
setGifUrl("");
setCommentBody(comment);
};
const selectGif = (e) => {
setGifUrl(e.images.downsized_large.url);
setCommentBody("");
// you wont be able to add text comment with a gif, it will look weird :(
};
const handleClickOpen = () => {
setOpenModal(true);
};
const handleCloseModal = () => {
setOpenModal(false);
};
const commentSubmit = (e: any, id: number) => {
e.preventDefault();
const formData = {
comment_body,
id,
gifUrl,
};
props.postComment(formData);
setCommentBody("");
setOpenForm(false);
console.log(divRef);
window.scrollTo(0, divRef.current.offsetTop);
};
const { post, currentUser, getNotifications } = props;
return (
<Fragment>
{getNotifications && <ToastContainer autoClose={1000} position={toast.POSITION.BOTTOM_RIGHT} />}
<Grid item={true} sm={12} md={12} style={{ margin: "20px 0px" }}>
<Paper style={{ padding: "20px" }}>
<Typography variant="h5" align="left">
<OurLink
style={{ fontSize: "16px" }}
to={{
pathname: `/post/${post.id}`,
state: { post },
}}
title={post.title}
/>
</Typography>
<Grid item={true} sm={12} md={12} style={{ padding: "30px 0px" }}>
<Typography align="left">{post.postContent.slice(0, 50)}</Typography>
</Grid>
<Avatar
style={{
display: "inline-block",
margin: "-10px -20px",
padding: "0px 30px 0px 20px",
}}
sizes="small"
src={post.author.gravatar}
/>
<Typography display="inline" variant="subtitle1" align="left">
<OurLink
to={{
pathname: `/profile/${post.author.username}`,
state: { post },
}}
title={post.author.username}
/>
</Typography>
<Typography align="right">Likes: {post.likeCounts}</Typography>
<Grid container={true} spacing={1} style={{ padding: "20px 0px" }}>
<Grid item={true} sm={10} lg={10} md={10} style={{ padding: "0px 0px" }}>
<Typography align="left">
{currentUser && currentUser.user && post.userId === currentUser.user.id ? (
<span style={{ cursor: "pointer" }} onClick={() => props.deletePost(post.id, post.userId)}>
<DeleteOutlineOutlinedIcon style={{ margin: "-5px 0px" }} color="primary" /> <span>Delete</span>
</span>
) : null}
</Typography>
</Grid>
<Grid item={true} sm={2} lg={2} style={{ padding: "0px 15px" }}>
<Typography align="right">
{Object.entries(currentUser).length === 0 ? (
<Fragment>
<span onClick={handleClickOpen}>
<FavoriteBorderIcon style={{ color: "red", cursor: "pointer" }} />
</span>
{openModal ? <OurModal open={openModal} handleClose={handleCloseModal} /> : null}
</Fragment>
) : (
<Fragment>
{post.likedByMe === true ? (
<span style={{ cursor: "pointer" }} onClick={() => props.dislikePost(post.id)}>
<FavoriteIcon style={{ color: "red" }} />
</span>
) : (
<span onClick={() => props.likePost(post.id)}>
<FavoriteBorderIcon style={{ color: "red", cursor: "pointer" }} />
</span>
)}
</Fragment>
)}
</Typography>
</Grid>
</Grid>
<Typography variant="h6" align="left">
{moment(post.createdAt).calendar()}
</Typography>
<Grid item={true} sm={12} lg={12} style={{ paddingTop: "40px" }}>
{Object.entries(currentUser).length === 0 ? (
<Fragment>
<Button onClick={handleClickOpen} variant="outlined" component="span" color="primary">
{"Write A Comment"}
</Button>
{openModal ? <OurModal open={openModal} handleClose={handleCloseModal} /> : null}
</Fragment>
) : (
<Fragment>
<Button onClick={writeComment} variant="outlined" component="span" color="primary">
{openForm ? "Close" : "Write A Comment"}
</Button>
</Fragment>
)}
{openForm ? (
<CommentForm
commentChange={(e: any) => commentChange(e.target.value)}
comment_body={comment_body}
onSubmit={(e) => commentSubmit(e, post.id)}
gifUrl={selectGif}
isGif={gifUrl}
/>
) : null}
{post.Comments.length > 0 ? (
<Fragment>
<Typography style={{ padding: "10px 0px", margin: "20px 0px" }}>Commments</Typography>
<CommentList ref={divRef} user={currentUser} deleteComment={props.deleteComment} userId={post.userId} postId={post.id} comments={post.Comments} {...props} />
{/* if show more hide show more button and show show less comments button */}
</Fragment>
) : (
<Grid item={true} sm={12} lg={12} style={{ padding: "30px 0px" }}>
<Typography>No Commments Yet</Typography>
</Grid>
)}
// <div ref={divRef}></div> works here
</Grid>
</Paper>
</Grid>
</Fragment>
);
}
export default PostItemContainer;
commentListContainer.tsx
import React from "react";
import List from "#material-ui/core/List";
import Typography from "#material-ui/core/Typography";
import CommentItem from "./../commentItem/CommentItem";
import moment from "moment";
import CommentAuthorData from "../commentAuthorData/commentAuthorData";
const ourStyle = {
margin: "15px",
};
const CommentListContainer = (props) => {
const { comment, openModal, handleClickOpen, handleCloseModal, isBold } = props;
return (
<List style={{ paddingBottom: "20px" }}>
<CommentAuthorData {...props} comment={comment} openModal={openModal} handleClickOpen={handleClickOpen} handleCloseModal={handleCloseModal} isBold={isBold} />
{/* want to call ref here but it returns undefined */}
<div ref={props.ref} style={ourStyle}>
<CommentItem comment={comment} user={props.user} postId={props.postId} {...props} />
<Typography style={{ fontSize: "12px" }} variant="body1" align="left">
{moment(comment.createdAt).calendar()}
</Typography>
</div>
</List>
);
};
export default CommentListContainer;
According to the docs: https://reactjs.org/docs/forwarding-refs.html you should use forwardRef for this:
// I used any for props, feel free to replace with your Props interface
const CommentListContainer: React.ForwardRefRenderFunction <HTMLDivElement, any> = (props, ref) => {
const { comment, openModal, handleClickOpen, handleCloseModal, isBold } = props;
return (
<List style={{ paddingBottom: "20px" }}>
<CommentAuthorData {...props} comment={comment} openModal={openModal} handleClickOpen={handleClickOpen} handleCloseModal={handleCloseModal} isBold={isBold} />
{/* here you pass your ref */}
<div ref={ref} style={ourStyle}>
<CommentItem comment={comment} user={props.user} postId={props.postId} {...props} />
<Typography style={{ fontSize: "12px" }} variant="body1" align="left">
{moment(comment.createdAt).calendar()}
</Typography>
</div>
</List>
);
};
// you use forwardRef here
export default React.forwardRef(CommentListContainer);
Here is the relevant TS definition for this: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L564
Your parent should remain unchanged.

react calls function for each item in array

I'm quite stuck on how to execute a function for a specific post.id, and not execute it for all items within the array.
The scenario
upon clicking show more comments, it shows more comments for each item in the array.
For example
and this
How can i make it so that upon show more comments, it shows more comments for that post only.
here is the current code, in which the logic is happening.
{post.Comments.length > 0 ? (
<Fragment>
<Typography style={{padding: "10px 0px", margin: "20px 0px"}}>Commments</Typography>
<CommentList showMore={showMore} comments={post.Comments} />
{/* if show more hide show more button and show show less comments button */}
{/* {isPost === post.id ? ( */}
<Fragment>
{post.Comments.length > 3 && showLessFlag === false && (
<Button onClick={ e => showComments(e, post.id)} variant="outlined" component="span" color="primary">
Show More Comments
</Button>
)}
{post.Comments.length > 3 && showLessFlag === true && (
<Button onClick={ e => showLessComments(e)} variant="outlined" component="span" color="primary">
Show Less Comments
</Button>
)}
</Fragment>
{/* ):(
null
)} */}
</Fragment>
):(
<Grid item={true} sm={12} lg={12} style={{ padding: "30px 0px"}}>
<Typography>No Commments Yet</Typography>
</Grid>
)}
fullCode(postList)
import Avatar from "#material-ui/core/Avatar";
import Button from "#material-ui/core/Button";
import Divider from "#material-ui/core/Divider";
import Grid from "#material-ui/core/Grid";
import Paper from "#material-ui/core/Paper";
import Typography from "#material-ui/core/Typography";
import DeleteOutlineOutlinedIcon from "#material-ui/icons/DeleteOutlineOutlined";
import FavoriteIcon from "#material-ui/icons/Favorite";
import FavoriteBorderIcon from "#material-ui/icons/FavoriteBorder";
import moment from "moment";
import React, { Fragment, useState } from "react";
import OurLink from "../../common/OurLink";
import CommentList from "../../forms/commentList/CommentList";
import CommentForm from "../../forms/comment/CommentForm";
export default function PostList(props: any) {
const [isComment, setIsComment] = useState(false);
const [showMore, setShowMore] = useState(3)
const [showLessFlag, setShowLessFlag] = useState(false);
const [comment_body, setCommentBody] = useState('');
const [isPost, setIsPost] = useState(null);
const writeComment = (id) => {
setIsComment(isComment ? "" : id);
};
const showComments = (e, id) => {
e.preventDefault();
setShowMore(12);
setShowLessFlag(true);
// setIsPost(isPost ? "" : id)
}
const showLessComments = (e) => {
e.preventDefault();
setShowMore(3);
setShowLessFlag(false);
}
const commentSubmit = (e: any, id:number) => {
e.preventDefault();
const formData = {
comment_body,
postId: id
};
if(comment_body.length > 6 ){
if(props.postComment(formData)){
setIsComment(false)
setCommentBody('')
}
}else{
alert("Comment must be at least 6 characters")
}
};
const { posts, currentUser} = props;
console.log(isPost)
return posts.length > 0 ? (
posts.map((post, i) => (
<Fragment key={i}>
<Grid item={true} sm={12} md={12} style={{ margin: "20px 0px" }}>
<Paper style={{ padding: "20px",}}>
<Typography variant="h5" align="left">
<OurLink to={{
pathname: `/post/${post.id}`,
state: { post },
}}
title={post.title}
/>
</Typography>
<Grid item={true} sm={12} md={12} style={{ padding: "30px 0px"}} >
<Typography align="left">{post.postContent.slice(0, 30)}</Typography>
</Grid>
<Avatar
style={{
display: "inline-block",
margin: "-10px -20px",
padding: "0px 30px 0px 20px",
}}
sizes="small"
src={post.author.gravatar}
/>
<Typography display="inline" variant="subtitle1" align="left">
{post.author.username}
</Typography>
<Typography align="right">Likes: {post.likeCounts}</Typography>
{/* <span
style={{ cursor: "pointer" }}
onClick={() => props.likePost(post.id)}
>
{" "}
Like this post
</span>
<div style={{ margin: "20px 0px", cursor: "pointer" }}>
<span onClick={() => props.dislikePost(post.id)}>
Dislike this post
</span>
</div> */}
<Grid container={true} spacing={1} style={{ padding: "20px 0px"}}>
<Grid item={true} sm={10} lg={10} md={10} style={{ padding: "0px 0px"}}>
<Typography align="left">
{currentUser && currentUser.user && post.userId === currentUser.user.id ? (
<span style={{cursor: "pointer"}} onClick={() => props.deletePost(post.id)}>
<DeleteOutlineOutlinedIcon style={{ margin: "-5px 0px"}} color="primary" /> <span>Delete</span>
</span>
) : (
null
)}
</Typography>
</Grid>
<Grid item={true} sm={2} lg={2} style={{ padding: "0px 15px"}}>
<Typography align="right">
{post.likedByMe === true ? (
<span style={{ cursor: "pointer"}} onClick={() => props.dislikePost(post.id)}>
<FavoriteIcon style={{ color: "red" }}/>
</span>
) : (
<span onClick={() => props.likePost(post.id)}>
<FavoriteBorderIcon
style={{ color: "red", cursor: "pointer" }}
/>
</span>
)}
</Typography>
</Grid>
</Grid>
<Typography variant="h6" align="left">
{moment(post.createdAt).calendar()}
</Typography>
<Grid item={true} sm={12} lg={12} style={{ paddingTop: "40px"}}>
<Button onClick={() => writeComment(post.id)} variant="outlined" component="span" color="primary">
{isComment === post.id ? "Close" : "Write A Comment"}
</Button>
{isComment === post.id
? (
<CommentForm
commentChange={e => setCommentBody(e.target.value)}
comment_body={comment_body}
onSubmit={e => commentSubmit(e, post.id)}
/>
)
: null}
{post.Comments.length > 0 ? (
<Fragment>
<Typography style={{padding: "10px 0px", margin: "20px 0px"}}>Commments</Typography>
<CommentList showMore={showMore} comments={post.Comments} />
{/* if show more hide show more button and show show less comments button */}
{/* {isPost === post.id ? ( */}
<Fragment>
{post.Comments.length > 3 && showLessFlag === false && (
<Button onClick={ e => showComments(e, post.id)} variant="outlined" component="span" color="primary">
Show More Comments
</Button>
)}
{post.Comments.length > 3 && showLessFlag === true && (
<Button onClick={ e => showLessComments(e)} variant="outlined" component="span" color="primary">
Show Less Comments
</Button>
)}
</Fragment>
{/* ):(
null
)} */}
</Fragment>
):(
<Grid item={true} sm={12} lg={12} style={{ padding: "30px 0px"}}>
<Typography>No Commments Yet</Typography>
</Grid>
)}
</Grid>
</Paper>
</Grid>
</Fragment>
))
) : (
<div>
<Grid item={true} md={8}>
<Typography>No Posts yet</Typography>
</Grid>
</div>
);
}
CommentList
import Divider from "#material-ui/core/Divider";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import Typography from "#material-ui/core/Typography";
import Button from "#material-ui/core/Button";
import Grid from "#material-ui/core/Grid";
import moment from "moment";
import React, { Component } from "react";
const CommentList = (props: any) => {
return(
<div style={{ overflow:"scroll"}}>
{props.comments.slice(0, props.showMore).map((comment, i) => (
<div key={i}>
<List style={{ paddingBottom: "20px"}}>
<ListItem alignItems="center" style={{ padding: "0px"}}>
<Typography color="primary" align="left">
{comment.comment_body}
</Typography>
</ListItem>
<Typography style={{ padding: "0px 0px"}} variant="caption" align="left">{comment.author.username}</Typography>
<Typography style={{fontSize: "12px"}} variant="body1" align="left">{moment(comment.createdAt).calendar()}</Typography>
<Divider variant="fullWidth" component="li" />
</List>
</div>
))}
</div>
)
};
export default CommentList;
I pretty much just moved the show/show less logic to the commentlist component, and made this component into to a react hook component, instead of a state less component.
so now we have
Similar issue
how to prevent duplicate onChange values within map loop
CommentList
import Divider from "#material-ui/core/Divider";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import Typography from "#material-ui/core/Typography";
import Button from "#material-ui/core/Button";
import Grid from "#material-ui/core/Grid";
import moment from "moment";
import React, { Component, Fragment, useState } from "react";
export default function CommentList(props: any) {
const [showMore, setShowMore] = useState(3)
const [showLessFlag, setShowLessFlag] = useState(false);
const showComments = (e) => {
e.preventDefault();
setShowMore(12);
setShowLessFlag(true);
}
const showLessComments = (e) => {
e.preventDefault();
setShowMore(3);
setShowLessFlag(false);
}
return (
<Grid>
{props.comments.slice(0, showMore).map((comment, i) => (
<div key={i}>
<List style={{ paddingBottom: "20px" }}>
<ListItem alignItems="center" style={{ padding: "0px" }}>
<Typography color="primary" align="left">
{comment.comment_body}
</Typography>
</ListItem>
<Typography style={{ padding: "0px 0px" }} variant="caption" align="left">{comment.author.username}</Typography>
<Typography style={{ fontSize: "12px" }} variant="body1" align="left">{moment(comment.createdAt).calendar()}</Typography>
<Divider variant="fullWidth" component="li" />
</List>
</div>
))}
<Fragment>
{props.comments.length > 3 && showLessFlag === false ? (
<Button onClick={e => showComments(e)} variant="outlined" component="span" color="primary">
Show More Comments
</Button>
) : (
<Fragment>
{props.comments.length > 3 && (
<Button onClick={e => showLessComments(e)} variant="outlined" component="span" color="primary">
Show Less Comments
</Button>
)}
</Fragment>
)}
</Fragment>
</Grid>
)
};
postList(after clean up)
{post.Comments.length > 0 ? (
<Fragment>
<Typography style={{ padding: "10px 0px", margin: "20px 0px" }}>Commments</Typography>
<CommentList comments={post.Comments} />
{/* if show more hide show more button and show show less comments button */}
</Fragment>
) : (
<Grid item={true} sm={12} lg={12} style={{ padding: "30px 0px" }}>
<Typography>No Commments Yet</Typography>
</Grid>
)}

react fires events for all items and the item selected

I'm trying to fire the onClick event for the item that i clicked on. Not for all items.
<Button onClick = {this.writeComment} varient="outlined" component="span" color="primary">
{!this.state.isComment ? "Write Comment": "Close"}
</Button>
{this.state.isComment ? <Comment onSubmit={this.commentSubmit} commentBody={this.state.comment_body} commentChange={this.handleCommentChange}/> : null}
writeComment function
writeComment = (e) => {
e.preventDefault();
this.setState({
isComment:!this.state.isComment
})
}
this should render a comment form for that item only, not for all items. This is within a map loop.
{this.state.images.length > 0 ? (
this.state.images.map( (img, i) => (
<Grid item sm={12} md={12} key={i} style={{ margin: '30px 0px'}}>
<Paper style={{padding:'20px 20px'}}>
{/* // empty image_title */}
<Typography style={{ padding: '30px 5px', letterSpacing:'8px', textTransform:'uppercase'}} variant="h4" align="center">{img.image_title}</Typography>
<Divider style={{ width: '150px', margin:'10px auto', backgroundColor:'#000000'}} variant="middle" />
<Image image_url={img.img_url} />
<Typography variant="h6" align="center">{img.user.username}</Typography>
<Typography variant="h6" align="center">{moment(img.created_at).calendar()}</Typography>
<Button onClick = {this.writeComment} varient="outlined" component="span" color="primary">
{!this.state.isComment ? "Write Comment": "Close"}
</Button>
{this.state.isComment ? <Comment onSubmit={this.commentSubmit} commentBody={this.state.comment_body} commentChange={this.handleCommentChange}/> : null}
<Button onClick={() => this.deleteImg(img.id)} variant="outlined" component="span" color="primary">
Delete
</Button>
</Paper>
</Grid>
))
) : (
<div>
<Grid item md={8}>
<Typography>No Images yet</Typography>
</Grid>
</div>
)}
</Grid>
{/* Images */}
</Grid>
Full code
import React, { Component } from "react";
import Button from '#material-ui/core/Button';
import TextField from '#material-ui/core/TextField';
import Grid from '#material-ui/core/Grid';
import Typography from '#material-ui/core/Typography';
import Paper from '#material-ui/core/Paper';
import ImageUploader from 'react-images-upload';
import Divider from '#material-ui/core/Divider';
import Axios from '../Axios';
import Image from './Image';
import moment from 'moment';
import Comment from './Comment';
class Dashboard extends Component{
constructor(props){
super(props);
this.state = {
image_url: 'http://www.conservewildlifenj.org/images/artmax_1001.jpg',
images: [],
description:'',
upload:false,
isComment:false,
comment_body:''
}
}
handleUpload = file => {
const data = new FormData()
const image = file[0]
// console.log(this.state.description)
// data.append('ourImage', this.state.description)
data.append('ourImage',image, this.state.description )
Axios.post('/images/upload', data).then((response) => {
const newImage = {...response.data}
console.log(newImage);
//update component-state
this.setState({
description:'', // resets title after upload
images: [
{
id: newImage[0].id,
user:{
username: newImage[0].user.username
},
image_title: newImage[0].image_title,
img_url: newImage[0].img_url,
created_at: new Date().toLocaleString().replace(',', ''),
updated_at: new Date().toLocaleString().replace(',', '')
},
...this.state.images
]
})
});
}
handleChange = (e) => {
// e.preventDefault();
this.setState({
[e.target.name]: e.target.value
})
// console.log(this.state.description)
}
handleCommentChange = (e) => {
this.setState({
comment_body: e.target.value
})
}
componentWillMount(){
Axios.get('/images/uploads').then( (response) => {
// let img;
// let imgTitle;
Object.keys(response.data).forEach( (key) => {
console.log(response.data[key]);
this.setState({
images:[ ...this.state.images, response.data[key]]
})
console.log(this.state.images);
});
})
}
componentDidUpdate(prevProps, prevState) {
if (this.state.images.length !== prevState.images.length) {
console.log(this.state.images);
}
// debugger;
}
onUploadClick = (e) => {
e.preventDefault();
this.setState({
upload: !this.state.upload
})
}
deleteImg = (id) => {
Axios.post(`/images/delete/${id}`).then( () => {
this.setState({
images: [ ...this.state.images.filter(img => img.id !== id)]
})
})
}
writeComment = (e) => {
e.preventDefault();
this.setState({
isComment:!this.state.isComment
})
}
commentSubmit = (e) => {
e.preventDefault();
console.log(this.state.comment_body);
// Axios.post('/images/newComment', this.state.comment_body).then( (response )=> {
// const newComment = { ...response.data};
// console.log(newComment);
// this.setState({
// comment_body: ''
// })
// })
}
render(){
const uploader = (
<ImageUploader
withIcon={true}
withPreview={true}
onChange={this.handleUpload}
singleImage={true}
buttonText='Upload an image'
imgExtension={['.jpg', '.gif', '.png', '.gif']}
maxFileSize={5242880}
/>
)
return(
<div>
<Grid container justify="center" spacing={16}>
<Grid item sm={8} md={6} style={{ margin: '40px 0px', padding: '0px 30px'}}>
<Typography align="center" variant="h6">
Welcome to the Dashboard
</Typography>
<Button onClick={this.onUploadClick} variant="outlined" component="span" color="primary">
{/* toggle between Upload or Close
Will be upload by default, else if upload is clicked, close will show.
*/}
{!this.state.upload ? "Upload": "Close"}
</Button>
<br></br>
<br></br>
{this.state.upload ? (
<div>
<TextField
id="outlined-name"
label="Image Title"
name="description"
type="text"
required={true}
fullWidth
style={{ borderRadius: '0px'}}
className=""
value={this.state.description}
onChange={this.handleChange}
margin="normal"
/>
<br></br>
<br></br>
{/* so here what we are saying, if this text field is FILLED show the uploader component
else hide it.
*/}
{this.state.description ? uploader : null}
</div>
):(
null
)}
{this.state.images.length > 0 ? (
this.state.images.map( (img, i) => (
<Grid item sm={12} md={12} key={i} style={{ margin: '30px 0px'}}>
<Paper style={{padding:'20px 20px'}}>
{/* // empty image_title */}
<Typography style={{ padding: '30px 5px', letterSpacing:'8px', textTransform:'uppercase'}} variant="h4" align="center">{img.image_title}</Typography>
<Divider style={{ width: '150px', margin:'10px auto', backgroundColor:'#000000'}} variant="middle" />
<Image image_url={img.img_url} />
<Typography variant="h6" align="center">{img.user.username}</Typography>
<Typography variant="h6" align="center">{moment(img.created_at).calendar()}</Typography>
<Button onClick = {this.writeComment} varient="outlined" component="span" color="primary">
{!this.state.isComment ? "Write Comment": "Close"}
</Button>
{this.state.isComment ? <Comment onSubmit={this.commentSubmit} commentBody={this.state.comment_body} commentChange={this.handleCommentChange}/> : null}
<Button onClick={() => this.deleteImg(img.id)} variant="outlined" component="span" color="primary">
Delete
</Button>
</Paper>
</Grid>
))
) : (
<div>
<Grid item md={8}>
<Typography>No Images yet</Typography>
</Grid>
</div>
)}
</Grid>
{/* Images */}
</Grid>
</div>
)
}
}
export default Dashboard;
onClick ={() => this.writeComment(image.id)} //id of your item
writeComment = (id) => {
this.setState({
isComment: this.state.isComment ? '' : id // check if you state is filled to toggle on/off comment
})
}
{this.state.isComment === image.id ? <Comment onSubmit={this.commentSubmit} commentBody={this.state.comment_body} commentChange={this.handleCommentChange}/> : null}

Resources