How can I get the correct ID assigned to the delete function

Viewed 70

I am learning while creating a test app that lets you create posts from a form. I have been stuck on the delete function going on five days now.

I was able to correctly map posts to print out the post_body to each card but when trying to delete it is always removing the last item in the database.

My guess is this has something to do with props and I have spent several days trying out different ways to pass props down through a functional component but no luck.

As seen in the screenshot, In the return I printed out the post_id for each card so you can see the proper ID is assigned for each card. However, once you get into the popover component the post_ID seems to always take on the value of the very bottom post.

Any direction is appreciated.

(note: I'm sure this code is pretty sloppy and I probably shouldn't be mapping over such a huge block of code. I may try refactoring once I figure out how these props should be working)

const ListPosts = (props) => {

  const [posts, setPosts] = useState([]);

  // Delete Post
  const deletePost = async id => {
    try {
        const deletePost = await fetch(`http://localhost:3000/posts/${id}`, {
          method: "DELETE"
      });
  
      setPosts(posts.filter(post => post.post_id !== id));  
      console.log(deletePost);
    } catch (err) {
      console.error(err.message);
    }
  }

  // Options Popover
  const [show, setShow] = useState(false);
  const [target, setTarget] = useState(null);
  const ref = useRef(null);

  const handleClick = (event) => {
    setShow(!show);
    setTarget(event.target);
  }
  

  // Get Posts Function
  const getPosts = async() => {
    try {
      
      const response = await fetch("http://localhost:3000/posts")
      const jsonData = await response.json()

      setPosts(jsonData);

    } catch (err) {
      console.error(err.message)
    }
  };

  useEffect(() => {
    getPosts();
  }, []);

  console.log(posts);


  return (
    <Fragment>
      {/* Map Post Text */}
      {posts.map(post => (
      <Card className="post-card" style={{ marginTop: "15px" }} key={post.post_id}> 
        <Card.Body>
          <div className="post-container">
            <div className="post-header row">
              <div className="user-photo-icon col-1"></div>
              <div className="user-names col-9"></div>
              <div className="options-menu col-2 ">
                <div ref={ref}>
                  <Button className="options-btn-popover" onClick={handleClick}>
                    <FontAwesomeIcon icon={ faEllipsisH } color="#848484" size="1x" className="options-icon" />
                  </Button>
                  
                  {/* Placed to show the point at which the ID's are still correct */}
                  {post.post_id}

                  <Overlay
                    show={show}
                    target={target}
                    placement="left"
                    container={ref.current}
                  >
                    <Popover className="shadow-sm" id="popover-contained" >

                      {/* Placed to show that now all id's show the post_id of the last post */}
                      {post.post_id}

                      <Popover.Content>
                        <div className="mb-2">
                          <Button className="options-btn-popover">
                            <FontAwesomeIcon icon={ faPencilAlt } size="1x" className="post-options-icon"/> 
                            Edit Post
                          </Button>
                        </div>
                        <div>
                          <Button className="options-btn-popover" onClick={() => deletePost(post.post_id)}>
                          <FontAwesomeIcon icon={ faTrashAlt } color="" size="1x" className="post-options-icon" /> 
                            Delete Post
                          </Button>
                        </div>
                      </Popover.Content>
                    </Popover>  
                  </Overlay>
                </div>
              </div>
            </div>
            <div className="post-text">{post.post_body}</div>
            <div className="post-media"></div>
            <div className="post-actions"></div>
            <div className="post-comments">
              <div className="post-subcomments"></div>
            </div>
          </div>
        </Card.Body>  
      </Card>
      ))}
    </Fragment>
  )
};

Here's a screenshot:

post list with post Id's

3 Answers

Issue

I suspect it is because you've only a single show state that when toggled opens all the popovers. All the popovers open but they are all positioned relative to the target element, they all overlay one another and the last one is on top.

Solution

I suggest storing the current post id in the "show" state and conditionally check/match that to open a specific popover.

  1. Start with null initial state:

    const [show, setShow] = useState(null);
    
  2. Update the click handler to consume a post id and curry the event object. Set the show state to the currently clicked on post id.

    const handleClick = (postId) => (event) => {
      setShow(postId);
      setTarget(event.target);
    }
    
  3. Pass the post.post_id when mapping the buttons.

    <Button
      className="options-btn-popover"
      onClick={handleClick(post.post_id)} // <-- pass post id
    >
      <FontAwesomeIcon
        icon={faEllipsisH}
        color="#848484"
        size="1x"
        className="options-icon"
      />
    </Button>
    
  4. Check the current post id when mapping the overlay/popover. If the current post id matched what is stored in show state then evaluate true, otherwise false.

    <Overlay
      show={show === post.post_id} // <-- compare post id to show state
      target={target}
      placement="left"
      container={ref.current}
    >
      <Popover className="shadow-sm" id="popover-contained" >
        <Popover.Content>
          ...
        </Popover.Content>
      </Popover>  
    </Overlay>
    
  5. Clear the show state when delete operation has completed. Functional state update to correctly update from the previous state (not the state from the render cycle the callback was triggered in.

    const deletePost = async id => {
      try {
        const deletePost = await fetch(`http://localhost:3000/posts/${id}`, {
          method: "DELETE"
        });
    
        setPosts(posts => posts.filter(post => post.post_id !== id)); // <-- functional update 
        console.log(deletePost);
      } catch (err) {
        console.error(err.message);
      } finally {
        setShow(null); // <-- reset back to null
      }
    }
    

Could you try using currying to accomplish this?

Below is your code with a small tweak that I made. When we declare the deletePost you will notice that it takes the id as a param and then calls another function. Then when you call this delete function you no longer need the () => before deletePost.

TLDR: currying lets you pass in the values before execution time.

const ListPosts = (props) => {

  const [posts, setPosts] = useState([]);

  // Delete Post
  const deletePost = id => async => {
    try {
        const deletePost = await fetch(`http://localhost:3000/posts/${id}`, {
          method: "DELETE"
      });
  
      setPosts(posts.filter(post => post.post_id !== id));  
      console.log(deletePost);
    } catch (err) {
      console.error(err.message);
    }
  }

  // Options Popover
  const [show, setShow] = useState(false);
  const [target, setTarget] = useState(null);
  const ref = useRef(null);

  const handleClick = (event) => {
    setShow(!show);
    setTarget(event.target);
  }
  

  // Get Posts Function
  const getPosts = async() => {
    try {
      
      const response = await fetch("http://localhost:3000/posts")
      const jsonData = await response.json()

      setPosts(jsonData);

    } catch (err) {
      console.error(err.message)
    }
  };

  useEffect(() => {
    getPosts();
  }, []);

  console.log(posts);


  return (
    <Fragment>
      {/* Map Post Text */}
      {posts.map(post => (
      <Card className="post-card" style={{ marginTop: "15px" }} key={post.post_id}> 
        <Card.Body>
          <div className="post-container">
            <div className="post-header row">
              <div className="user-photo-icon col-1"></div>
              <div className="user-names col-9"></div>
              <div className="options-menu col-2 ">
                <div ref={ref}>
                  <Button className="options-btn-popover" onClick={handleClick}>
                    <FontAwesomeIcon icon={ faEllipsisH } color="#848484" size="1x" className="options-icon" />
                  </Button>
                  
                  {/* Placed to show the point at which the ID's are still correct */}
                  {post.post_id}

                  <Overlay
                    show={show}
                    target={target}
                    placement="left"
                    container={ref.current}
                  >
                    <Popover className="shadow-sm" id="popover-contained" >

                      {/* Placed to show that now all id's show the post_id of the last post */}
                      {post.post_id}

                      <Popover.Content>
                        <div className="mb-2">
                          <Button className="options-btn-popover">
                            <FontAwesomeIcon icon={ faPencilAlt } size="1x" className="post-options-icon"/> 
                            Edit Post
                          </Button>
                        </div>
                        <div>
                          <Button className="options-btn-popover" onClick={deletePost(post.post_id)}>
                          <FontAwesomeIcon icon={ faTrashAlt } color="" size="1x" className="post-options-icon" /> 
                            Delete Post
                          </Button>
                        </div>
                      </Popover.Content>
                    </Popover>  
                  </Overlay>
                </div>
              </div>
            </div>
            <div className="post-text">{post.post_body}</div>
            <div className="post-media"></div>
            <div className="post-actions"></div>
            <div className="post-comments">
              <div className="post-subcomments"></div>
            </div>
          </div>
        </Card.Body>  
      </Card>
      ))}
    </Fragment>
  )
};

I think the problem is that you are creating Overlay/Popover component inside loop, try to move it out of the loop. You can then use const [selectedPost, selectPost] = useState() to track data which should be rendered in overlay. button is clicked. Also adjust onClick={handleClick} to call selectPost(post).

Related