adrianhajdin / project_mern_memories

This is a code repository for the corresponding video tutorial. Using React, Node.js, Express & MongoDB you'll learn how to build a Full Stack MERN Application - from start to finish. The App is called "Memories" and it is a simple social media app that allows users to post interesting events that happened in their lives.
https://youtube.com/playlist?list=PL6QREj8te1P7VSwhrMf3D3Xt4V6_SRkhu
4.96k stars 1.83k forks source link

Button embedded into ButtonBase makes the Home page fall apart and also makes the edit button unsuable #103

Closed szaboako closed 2 years ago

szaboako commented 2 years ago

Hello,

In the components/Posts/Post/Post.js file there is a Button embedded into a ButtonBase component that creates a warning and also destroys the Home page where these components are used via the Posts component.

The warning says this: validateDOMNesting(...): <button> cannot appear as a descendant of <button>

If I change the ButtonBase to a simple div (it loses a lot of styling) the edit button is still unusable because it registers the parent divs onClick first.

import React, { useState } from "react"
import { Card, CardActions, CardContent, CardMedia, Button, Typography, ButtonBase } from '@material-ui/core'
import ThumbUpAltIcon from '@material-ui/icons/ThumbUpAlt'
import ThumbUpAltOutlined from "@material-ui/icons/ThumbUpAltOutlined"
import DeleteIcon from '@material-ui/icons/Delete'
import MoreHorizIcon from '@material-ui/icons/MoreHoriz'
import moment from 'moment'
import { useDispatch } from "react-redux"
import { useNavigate } from "react-router-dom"

import useStyles from './styles'
import { deletePost, likePost } from '../../../actions/posts'

const Post = ({ post, setCurrentId }) => {
    const user = JSON.parse(localStorage.getItem('profile'))
    const [likes, setLikes] = useState(post?.likes)
    const userId = user?.result?.googleId || user?.result?._id
    const hasLikedPost = post.likes.find((like) => like === userId)
    const classes = useStyles()
    const dispatch = useDispatch()
    const navigate = useNavigate()

    const handleLike = async () => {
        dispatch(likePost(post._id))

        if(hasLikedPost) {
            setLikes(post.likes.filter((id) => id !== userId))
        } else {
            setLikes([...post.likes, userId])
        }
    }

    const openPost = () => navigate(`/posts/${post._id}`)

    const Likes = () => {
        if (likes.length > 0) {
            return likes.find((like) => like === userId)
                ? (
                    <><ThumbUpAltIcon fontSize="small" />&nbsp;{likes.length > 2 ? `You and ${likes.length - 1} others` : `${likes.length} like${likes.length > 1 ? 's' : ''}` }</>
                ) : (
                    <><ThumbUpAltOutlined fontSize="small" />&nbsp;{likes.length} {likes.length === 1 ? 'Like' : 'Likes'}</>
                );
        }

        return <><ThumbUpAltOutlined fontSize="small" />&nbsp;Like</>;
    }

    return (
        <Card className={classes.card} raised elevation={6}>
            <ButtonBase className={classes.cardAction} onClick={openPost}>
                <CardMedia className={classes.media} image={post.selectedFile} title={post.title} />
                <div className={classes.overlay}>
                    <Typography variant="h6">{post.name}</Typography>
                    <Typography variant="body2">{moment(post.createdAt).fromNow()}</Typography>
                </div>
                {(user?.result?.googleId === post?.creator || user?.result?._id === post?.creator) && (
                    <div className={classes.overlay2}>
                        <Button style={{color: 'white'}} size="small" onClick={() => setCurrentId(post._id)}>
                            <MoreHorizIcon fontSize="medium" />
                        </Button>
                    </div>
                )}
                <div className={classes.details}> 
                    <Typography variant="body2" color="textSecondary">{post.tags.map((tag) => `#${tag} `)}</Typography>
                </div>
                <Typography className={classes.title} variant="h5" gutterBottom>{post.title}</Typography>
                <CardContent>
                    <Typography variant="body2" color="textSecondary" component="p">{post.message}</Typography>
                </CardContent>
            </ButtonBase>
            <CardActions className={classes.cardActions}>
                <Button size="small" color="primary" disabled={!user?.result} onClick={handleLike}>
                <Likes />
                </Button>
                {(user?.result?.googleId === post?.creator || user?.result?._id === post?.creator) && (
                    <Button size="small" color="primary" onClick={() => dispatch(deletePost(post._id))}>
                        <DeleteIcon fontSize="small" />
                        Delete
                    </Button>
                )}
            </CardActions>
        </Card>
    )
}

export default Post

How can I solve this?

szaboako commented 2 years ago

This issue actually had two parts:

the first is some styling was missing from the styles.js, that made the page fall apart (that solved the warning)

  cardAction: {
    display: 'block',
    textAlign: 'initial',
  },

the second was due to a thing called "bubbling" that basically means in this case that 1st the inner elements event fires then the outers so we have to stop the second event from happening in order to be able to edit our posts and not be navigated to the details page instead. To solve this I added a handleEdit function instead of the inline one. The event.stopPropagation stops the bubbling.

    const handleEdit = (e) => {
        setCurrentId(post._id)
        e.stopPropagation();
    }
<Button style={{color: 'white'}} size="small" onClick={handleEdit}>
    <MoreHorizIcon fontSize="medium" />
</Button>
JackyTang0516 commented 1 year ago

You saved my world, appreciate for your help!