Im having troubles to expand and contract a Collapse Component from MaterialUI since Im mapping and array and iterating the same component, when i press the collapse button, all components expands/contracts at the same time ( I suppose that Im not providing an identifier to point where the collapse function should be used),Im currently Using an State to control the collapse action:
const [expanded, setExpanded] = useState(false);
This is the return where I iterate the component using map on RecetasAll object,
return (
<React.Fragment key={RecetasAll.id}>
<Card className="searchItem" sx={{ maxWidth: 345 }}>
<CardHeader
action={<IconButton aria-label="settings"></IconButton>}
title={RecetasAll.titulo}
/>
<h4
className="Dieta"
style={{
backgroundColor: color(RecetasAll.Tiporeceta.tipoReceta),
}}
>
{RecetasAll.Tiporeceta.tipoReceta}
</h4>
<span className="Calorias">{RecetasAll.informacionNutricional}</span>
<CardMedia
component="img"
height="194"
image={RecetasAll.imagen}
alt="Paella dish"
/>
<CardContent>
{RecetasAll.Productos.map((Productos) => {
return (
<React.Fragment key={Productos.id}>
<Typography variant="body2" color="text.secondary">
{Productos.producto}
</Typography>
</React.Fragment>
);
})}
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<FavoriteIcon />
</IconButton>
<ExpandMore
expand={expanded}
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
>
<ExpandMoreIcon />
</ExpandMore>
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent id={RecetasAll.id}>
<Typography paragraph>Preparacion:</Typography>
<Typography paragraph>{RecetasAll.pasos}</Typography>
<Button
href="#contained-buttons"
variant="contained"
onClick={handleSearch}
>
Ver mas
</Button>
</CardContent>
</Collapse>
</Card>
</React.Fragment>
);
});
return <>{itemRecetas}</>;
}
Im triying to set an id property to the CardContent since its the child of the Collapse component
id={RecetasAll.id}
this is the function Im using to expand or collapse but I dont know how to get the id properly to compare its value with expanded state:
const handleExpandClick = (e) => {
let clickedItemId = e.currentTarget.id;
if (expanded === clickedItemId) {
setExpanded(!expanded);
} else {
setExpanded(clickedItemId);
}
};
You could refactor every card into a new component and that way you can have a state to open/close the individual card. When iterating you can pass in the RecetasAll.
const MyCard = ({ RecetasAll }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => {
setIsExpanded(prevIsExpanded => !prevIsExpanded);
};
return (
...
<ExpandMore
expand={isExpanded}
onClick={toggleExpanded}
aria-expanded={isExpanded}
>
...
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
...
);
};
If you don't want to use a new component you could store all the ids of the expanded cards in a state. Based on if the id is in the array the card will be expanded or collapsed.
const [expandedIds, setExpandedIds] = useState([]);
const toggleExpanded = (id) => {
setExpandedIds((prevExpandedIds) => {
// if id is already in array remove
if (prevExpandedIds.includes(id))
return prevExpandedIds.filter((i) => i !== id);
// else add to array
return [...prevExpandedIds, id];
});
};
return (
...
<ExpandMore
expand={expandedIds.includes(RecetasAll.id)}
onClick={() => toggleExpanded(RecetasAll.id)}
aria-expanded={expandedIds.includes(RecetasAll.id)}
>
...
<Collapse in={expandedIds.includes(RecetasAll.id)} timeout="auto" unmountOnExit>
...
)
Related
You have to Imagine I have a node inside this node are comments(messages) and now I want the user who made the comment to be able to edit or even delete his own comment directly, but only the user who created it.
I honestly don't know if it works that way, but I hope someone can help me with this.
Edit: I add a deleteHandler.
function Messages({ forum }) {
console.log(forum);
const dispatch = useDispatch();
const classes = useStyles();
const [message, setMessage] = useState("");
const messageList = useSelector((state) => state.messageList);
const userLogin = useSelector((state) => state.userLogin);
const { userInfo } = userLogin;
const { messages } = messageList;
const handleClick = async () => {
const finalMessage = `${userInfo.userName}: ${message}`;
await dispatch(createMessageAction(forum._id, finalMessage));
dispatch(listMessage());
setMessage("");
};
const deleteHandler = (id) => {
if (window.confirm("Are you sure? you want to delete")) {
dispatch(deleteMessageAction(id));
}
};
useEffect(() => {
dispatch(listMessage());
}, []);
console.log(messages);
return (
<div>
<div className={classes.messagesOuterContainer}>
<div className={classes.messagesInnerContainer}>
<Typography gutterBottom variant="h6">
Comments
</Typography>
{messages
?.filter((message) => message.forumID === forum._id)
?.map((c) => (
<Typography key={c._id} gutterBottom variant="subtitle1">
<strong>{c.messageText} </strong>
<EditIcon />
<IconButton aria-label="delete">
<DeleteIcon onClick={() => deleteHandler(message._id)}/>
</IconButton>
</Typography>
))}
</div>
{userInfo?.userName && (
<div style={{ width: "70%" }}>
<Typography gutterBottom variant="h6">
Comments
</Typography>
<TextField
fullwidth="false"
rows={4}
variant="outlined"
label="add a Comment"
multiline
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<Button
style={{ marginTop: "10px" }}
fullwidth="false"
disabled={!message}
variant="contained"
color="primary"
onClick={handleClick}
>
Send
</Button>
</div>
)}
</div>
</div>
);
My MessageList state:
>messageList
>messages
>0: {_id:"....", forumID:"..", messageText:"...", user:".."
>1: .....
You must have information about the author of the comment. Assuming each entry in messages has that information, create a conditional that checks to see if the current user's id is the same as the message's author id.
Something along the lines of this.
{messages
?.filter((message) => message.forumID === forum._id)
?.map((c) => (
<Typography key={c._id} gutterBottom variant="subtitle1">
<strong>{c.messageText} </strong>
{message.userId === userLogin.id && ( // modify keys names accordingly
<>
<EditIcon />
<IconButton aria-label="delete">
<DeleteIcon />
</IconButton>
</>
)}
</Typography>
))}
I am working on a Frontend Mentor project and I am trying to pass the hover status from the parent component (App) to the child component (Listing).
You can see that I have created a state object called hover inside the App component and passed it to the Listing component but when the hover object updates the css style is not applied as it should be on the Typography element inside the Listing component. Or at least there isn't a re-render if it does.
App.js
let [hover, updateHover] = useState(false);
updateHover = () => {
if(hover === false){hover = true; console.log(hover); return(0);}
else{hover = false; console.log(hover); return;}
}
return (
<ThemeProvider theme={ theme }>
<div style={{backgroundColor:'hsl(180, 52%, 96%)',}}>
<div style={{backgroundImage:`url(${headerImage})`, backgroundSize:'cover', height:'10rem'}}></div>
<Box display="flex" justifyContent="center" alignItems="center">
<Box style={{width:'70%', marginTop:'5rem'}}>
<Card style={styles.listing} onMouseEnter={updateHover} onMouseLeave={updateHover}>
<CardActionArea href="#" className="listingHover">
<Listing id="one" hover={hover} />
</CardActionArea>
</Card>
</Box>
</Box>
</div>
</ThemeProvider>
);
}
export default App;
Listing.js
function Listing(props) {
let id = props.id
let hover = props.hover
return (
<React.Fragment>
<Box className="listing" display="flex" sx={{backgroundColor:'#fff', width:'100%', height:'7.3rem'}}>
<Box>
<Typography variant="h4"><Link className={hover ? 'jobTitle': null} href="#" color="secondary" underline="none">{jobs[id].name}</Link></Typography>
</Box>
</Box>
</Box>
</React.Fragment>
)
}
export default Listing
I think your problem here is that you re declared updateHover.
You should change the name and have something like
const [hover, setHover] = useState(false);
const updateHover = () => {
if (!hover) {
console.log(hover);
setHover(true);
return(0)
} else {
setHover(false)
return;
}
}
as an aside why are you returning from the function? do you need the console.log? a cleaner option would be
const [hover, setHover] = useState(false);
const updateHover = () => setHover(!hover) // flips the current value i.e same as if (hover === true) setHover(false) else setHover(true);
return (
<ThemeProvider theme={ theme }>
<div style={{backgroundColor:'hsl(180, 52%, 96%)',}}>
<div style={{backgroundImage:`url(${headerImage})`, backgroundSize:'cover', height:'10rem'}}></div>
<Box display="flex" justifyContent="center" alignItems="center">
<Box style={{width:'70%', marginTop:'5rem'}}>
<Card style={styles.listing} onMouseEnter={updateHover} onMouseLeave={updateHover}>
<CardActionArea href="#" className="listingHover">
<Listing id="one" hover={hover} />
</CardActionArea>
</Card>
</Box>
</Box>
</div>
</ThemeProvider>
);
}
You are wrong
change state in the React.
Try like this.
const [hover, setHover] = useState(false);
updateHover = () => {
setHover(!hover)
}
or
const [hover, setHover] = useState(false);
return (
<ThemeProvider theme={ theme }>
<div style={{backgroundColor:'hsl(180, 52%, 96%)',}}>
<div style={{backgroundImage:`url(${headerImage})`, backgroundSize:'cover', height:'10rem'}}></div>
<Box display="flex" justifyContent="center" alignItems="center">
<Box style={{width:'70%', marginTop:'5rem'}}>
<Card style={styles.listing} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>
<CardActionArea href="#" className="listingHover">
<Listing id="one" hover={hover} />
</CardActionArea>
</Card>
</Box>
</Box>
</div>
</ThemeProvider>
);
I have an array of items that i want to show with a map function, and every item is shown as a card.
I'm trying to show two kinds of cards with a different content, one if "isHover" is false, and the other if it true using onMouseEnter/onMouseOver.
I made "isHover" as an array in order to know which item to show/hide.
(The "isHover" array has the same length that the items' array has).
The problem is that when I hover one card it dissappears and nothing is shown in place of it. :(
The code:
function TeachersShow(props) {
const [isHover, setIsHover] = useState(null);
const updateIsHover = (index, isHover1) => {
let newArray = isHover;
newArray[index] = isHover1;
setIsHover([...newArray]);
console.log(isHover[index]);
};
return (
<div>
{isHover[index] === false && (<Card className="teacher-card"
onMouseEnter={() => { updateIsHover(index, true) }}
key={index}
item={item}
onClick={() => navigateToTeacher(item)}
>
<Card.Img className="teachersImg" src={item.photoURL}>
</Card.Img>
<Card.Title className=" teachersName">
{item.username}
</Card.Title>
</Card>)}
{isHover[index] === true && (
<Card className="card-hover"
onMouseleave={() => { updateIsHover(index, false) }}
key={index}
item={item}
onClick={() => navigateToTeacher(item)}
>
<Card.Title className=" teachersName">
{item.username}
</Card.Title>
<Card.Subtitle className="proTeacher">
{`${item.profession} teacher`}
</Card.Subtitle>
<Card.Text className="teacherDesc">
{item.teacher_description}
</Card.Text>
</Card>)}
</Col>
))}
<Col></Col>
</Row>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(TeachersShow);
It's little hard to tell what's wrong, since a lot of information is missing from the code. But try to create a component Let's say <Teacher />, and let it be responsible for hovering action. Try this:
function Teacher(item) {
const [hover, setHover] = useState(false);
const renderCardData = () => {
if (!hover) {
return (
<Card.Img className="teachersImg" src={item.photoURL} />
<Card.Title className="teachersName">
{item.username}
</Card.Title>
);
}
return (
<Card.Title className=" teachersName">
{ item.username }
</Card.Title>
<Card.Subtitle className="proTeacher">
{ `${item.profession} teacher` }
</Card.Subtitle>
<Card.Text className="teacherDesc">
{ item.teacher_description }
</Card.Text>
);
};
return (
<Card
className={ hover
? 'card-hover'
: 'teacher-card' }
onMouseEnter={ () => setHover(true) }
onMouseLeave={ () => setHover(false) }
>
{ renderCardData() }
</Card>
);
}
export default Teacher;
And you render it like that:
function TeachersList(teachers) {
return teachers.map(Teacher);
};
The state of a value set using React useState hook gets set to the proper value and then reset to null. Critical code below. The click event that sets the startDate to the current date and time is 3 components down from where startDate is initialized. When setStartDate did not work I created an arrow function, updateStartDate. Both had the same problem where the startDate was changed after the click event (witnessed per the console.log in the top component), but was null just before the next click event (per the console.log in the click event). This is not an async problem as I see the change made before subsequent click.
If this is something that just does not work please explain. I could probably fix with useReducer but prefer to keep the useState if there is something I can do to correct this... If not correctable then I would like to at least understand why it does not work so that I can avoid this problem in the future.
export const DisplayTicTacToeContainer = (props) => {
const [startDate, setStartDate]= useState();
const updateStartDate = (newDate) => {
setStartDate(newDate);
}
useEffect (() => {
setStartDate(null);
}, []);
useEffect(() => {
console.log( "displayTicTacToeContainer useEffect for change of startDate = ", startDate)
}, [startDate]);
return (
<DisplayTicTacToeMatch arrayOfMatchingItems ={arrayOfMatchingItems}
startDate={startDate}
setStartDate={setStartDate}
updateStartDate={updateStartDate}
/>);
}
//-----------------------------------------------
export const DisplayTicTacToeMatch = (props) => {
const { startDate,
setStartDate,
updateStartDate,
} = props;
useEffect(() => {
// Performs some prep and working fine.
}, []);
return (
<TicTacToe
startDate={startDate}
setStartDate={setStartDate}
updateStartDate={updateStartDate}
/>
);
}
//-----------------------------------------------
const TicTacToeContainer = (props) => {
const { startDate,
setStartDate,
updateStartDate,
} = props;
const [board, setBoard] = useState(<Board
updateStartDate={updateStartDate}
startDate={startDate}
setStartDate={setStartDate}/>);
return (
<Board/>
)
}
export default TicTacToeContainer;
I renamed the component to BoardComponent and the state variable to boardLayout. I included the full return portion of the BoardComponent below.
As I am still experiencing the problem I would agree with you that, "DisplayTicTacToeContainer is being mounted twice". Any thoughts on how I can avoid this from happening?
Other than this inability to setStartDate, everything is working fine.
//-----------------------------------------------
const Board = (props) => {
const { updateStartDate,
startDate,
setStartDate,
} = props;
return (
<>
<Grid container maxwidth="lg" alignItems="center" spacing={1}>
<Grid item xs={9}>
<Grid container alignItems="center">
<Grid item xs={9}>
<Typography variant = "body1">
First select a square. Once the "Inquiry" word or phrase appears below, find
the correct response in the column on the right and select that buttton. A correct
response will fill the square previously selected with an "O" or "X".
</Typography>
<div style={{ width: '100%' }}>
<Box
display="flex"
flexWrap="wrap"
p={1}
m={1}
bgcolor="background.paper"
css={{ maxWidth: 900 }}
>
<Box p={1} bgcolor="grey.300">
Inquiry : {inquiry}
</Box>
</Box>
<Box
display="flex"
flexWrap="wrap"
p={1}
m={1}
bgcolor="background.paper"
css={{ maxWidth: 900 }}
>
<Box p={1} bgcolor="grey.300">
Next move by : {currentPlayer}
</Box>
<Box p={1} bgcolor="grey.300">
{showStatus}
</Box>
</Box>
</div>
</Grid>
</Grid>
<MyAux>
{boardLayout.map((row, rowId) => {
const columns = row.map((column, columnId) => (
<Grid key={columnId} item>
<ButtonBase >
<Paper
onClick={(e) => {
clickSquareHandler(e);
}}
elevation={4}
data-coord={rowId + ':' + columnId}
id={"Square" + rowId.toString() + columnId.toString()}
className={classes.Paper}>
<Icon
className={classes.Icon}
style={{fontSize: 78}}>
</Icon>
</Paper>
</ButtonBase>
</Grid>
));
return (
<Grid
key={rowId}
className={classes.Grid}
container
spacing={2}>
{columns}
</Grid>)
})}
</MyAux>
</Grid>
<Grid item xs={3} >
<Paper className={classes.paper}>
<Typography variant = "body1">
Response Options
</Typography>
<ButtonGroup
orientation="vertical"
color="secondary"
aria-label="vertical outlined secondary button group"
>
{responseChoices.map((choice) => (
<Controls.Button
key ={choice.value}
text={choice.value}
variant="contained"
color = "secondary"
onClick={() => {
chooseChecker(choice);
}}
className={
response && response.value === choice.value ? "selected" : ""
}
disabled={!!selected[choice.value]}
fullWidth = "true"
size = "small"
/>
))}
</ButtonGroup>
</Paper>
</Grid>
</Grid>
</>
)
}
BoardContainer.propTypes = {
won: PropTypes.func,
size: PropTypes.number
};
export default BoardContainer;
At least, code below doesn't make much sense.
Please don't set state value as a component.
Also, try to name state variable different from components, since it will confuse you at some ppint.
const [board, setBoard] = useState(<Board
updateStartDate={updateStartDate}
startDate={startDate}
setStartDate={setStartDate}/>);
return (
<Board/>
)
Another possibility is that the DisplayTicTacToeContainer is being mounted twice, but I can't confirm it with the code provided.
I'm having a devil of a time getting this component to properly take an array and map out components based on it. Although I can see this array log to the console, for some reason, my component (in the dev environment) is just displaying blank. So, I know that ternary statement is going to true, but React isn't then mapping (and thus, creating JSX) based on the array.
Note: I've checked out other questions here on SO, and I haven't seen one that's quite like this. If I'm wrong, please point me in that direction!
Expected Behavior
I am successfully setting listings to an array with 11 objects in it, so the DOM should display 11 cards using this data.
Observed Behavior
The DOM will display <Skeleton />s at first, indicating that the ternary statement is flowing to the falsey code block, and then the DOM will just not display anything. White space.
Code Sample
Browse (Parent)
export default function Browse({ user }) {
// prevents useEffect from infinite looping
let [loaded, setLoaded] = useState(false)
let [listings, setListings] = useState(false) // Raw listings array pulled from DB
let [radius, setRadius] = useState(10000)
useEffect(() => {
// Waits for user to come in from Auth Context
if (typeof user.userDoc !== 'undefined' || listings.length < 1) {
if (loaded === false) {
let grabListings = async () => await getLocalListings().then(result => setListings(result))
grabListings()
}
if (listings.length > 1) {
setLoaded(true)
}
}
}, [user, listings])
console.log(listings) // successfully shows array in listings state
return (
<>
<Navbar />
{
listings
? listings.map(listing => {
return (
<BrowseItem
... // props successfully passed here
/>
)
})
: <>
<Container style={{ marginTop: '18px' }}>
<Skeleton animation="wave" />
<Skeleton animation="wave" />
<Skeleton animation="wave" />
<Skeleton animation="wave" />
<Skeleton animation="wave" />
</Container>
</>
}
<Footer />
</>
)
}
BrowseItem (Child)
export default function BrowseItem({
creatorProfilePic,
creatorUsername,
docId,
mainImgUrl,
productStory,
productTitle,
seoTitle,
user
}) {
...
let [favorited, setFavorited] = useState(null) // whether user has favorited this item
let [loaded, setLoaded] = useState(false)
/** Handles click of the favorite button.
* Routes to FirebaseCalls function based on state of favorited
*/
const handleFavorite = async (e) => {
e.preventDefault()
let result
if (favorited) {
result = await removeFavorite(docId, user.currentUid)
if (result === 'success') {
setFavorited(!favorited)
setSnackMessage('Removed from Favorites')
setOpen(true)
} else {
setSnackMessage('⚠️ There was an error removing this from Favorites')
setOpen(true)
}
} else {
result = await addFavorite(docId, user.currentUid)
if (result === 'success') {
setFavorited(!favorited)
setSnackMessage('✓ Added to Favorites')
setOpen(true)
} else {
setSnackMessage('⚠️ There was an error adding this to Favorites')
setOpen(true)
}
}
}
useEffect(() => {
if (!loaded) {
// Determines whether item should default to favorited
try {
let userFavorites = user.userDoc.favorites
if (userFavorites) {
if (userFavorites.some(id => id === docId)) {
setFavorited(true)
}
}
setLoaded(true)
} catch (error) {
console.log(error)
}
}
}, [user])
////////////////////////////////////////////////////////////////////////
///////////////////////////// SNACKS ///////////////////////////////////
////////////////////////////////////////////////////////////////////////
let [snackMessage, setSnackMessage] = useState('')
let [open, setOpen] = useState(null)
const handleSnackClose = (event, reason) => {
if (reason === 'clickaway') {
return
}
setOpen(false)
}
return (
<>
{
loaded
? <>
<Card>
<CardHeader
avatar={
<Avatar alt={`${creatorUsername} Profile Picture`} src={creatorProfilePic} />
}
action={
<IconButton>
<MoreHoriz />
</IconButton>
}
title={productTitle}
subheader={`${creatorUsername}`}
/>
<CardMedia
className={classes.media}
image={mainImgUrl}
title={productTitle}
onClick={handleImgTouch}
/>
<CardContent>
<Typography variant="body2" color="textSecondary" component="p">
{productStory}
</Typography>
</CardContent>
<CardActions disableSpacing>
<IconButton onClick={handleFavorite}>
{
favorited
? <Favorite color='primary' />
: <Favorite />
}
</IconButton>
<IconButton>
<Share />
</IconButton>
</CardActions>
</Card>
<Box mb={4}></Box>
</>
: <>
<Skeleton animation="wave" />
<Skeleton animation="wave" />
<Skeleton animation="wave" />
<Skeleton animation="wave" />
</>
}
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={open}
autoHideDuration={3000}
onClose={handleSnackClose}
message={snackMessage}
action={
<>
<Button onClick={handleFavorite} color='primary'>UNDO</Button>
<IconButton size="small" color="inherit" onClick={handleSnackClose}>
<Close fontSize="small" />
</IconButton>
</>
}
/>
</>
)
}
Stack
"next": "^9.4.4"
"react": "^16.8.6"
Testing in a Next.js Development Environment