reactjsreact-reduxreact-dom

onClick event not working on icon displayed on hover


React newbie here.

There's a ItemsList component which is basically a table displaying some information. I managed to display two icons (edit and delete) when the row in the table is hovered (the same as Gmail). And then I wanted to render a DeleteModal component, which is basically a delete confirmation, when the delete icon is clicked.

The table and the icons on hover

import styles from './ItemsList.module.scss';
import '../../../styles/buttons.scss';

const ItemsList = () => {

const dispatch = useDispatch();

    
    const handleOpenDeleteItemModal = () => {
        dispatch(SET_DELETE_ITEM_MODAL(true));
        dispatch(SET_SIDEBAR(false));
    };

    const handleMouseEnter = (e) => {
        e.currentTarget.lastElementChild.innerHTML = ReactDOMServer.renderToString(
            <>
                <BiMessageSquareEdit className={styles.list__icons} />
                <BsTrash
                    className={styles.list__icons}
                    onClick={handleOpenDeleteItemModal}
                />
            </>
        );
    };

    const handleMouseLeave = (e, createdAt) => {
        e.currentTarget.lastElementChild.innerHTML = ReactDOMServer.renderToString(
            <Moment format="DD/MM/YY">{createdAt}</Moment>
        );
    };

        return (
<div className={styles.list__table}>
                {!isLoading && items.length === 0 ? (
                    <p>No items found, please add an item.</p>
                ) : (
                    <table>
                        <thead>
                            <tr>
                                <th>Number</th>
                                <th>Name</th>
                                <th>Category</th>
                                <th>Price</th>
                                <th>Quantity</th>
                                <th>Value</th>
                                <th>Date</th>
                            </tr>
                        </thead>
                        <tbody>
                            {currentItems.map((item, index) => {
                                const { _id, name, category, price, quantity, createdAt } =
                                    item;
                                return (
                                    <tr
                                        onMouseEnter={(e) => handleMouseEnter(e)}
                                        onMouseLeave={(e) => handleMouseLeave(e, createdAt)}
                                        key={_id}
                                    >
                                        <td>{index + 1 + '.'}</td>
                                        <td>{shortenText(name, 15)}</td>
                                        <td>{category}</td>
                                        <td>
                                            {'£'}
                                            {price}
                                        </td>
                                        <td>{quantity}</td>
                                        <td>
                                            {'£'}
                                            {price * quantity}
                                        </td>
                                        <td>
                                            <Moment format="DD/MM/YY">{createdAt}</Moment>
                                        </td>
                                    </tr>
                                );
                            })}
                        </tbody>
                    </table>
                )}
            </div>)

}

CSS

.list__container {
    padding: 2rem;
    hr {
        border: 1px solid a.$hr;
    }
    .list__topSection {
        padding: 3rem 0 1.5rem 0;
        display: flex;
        align-items: center;
        justify-content: space-between;

        h3 {
            font-size: 1.5rem;
            font-family: a.$roboto;
            font-weight: a.$medium;
        }
    }
    .list__table {
        font-family: a.$roboto;
        table {
            border-collapse: collapse;
            width: 100%;

            font-size: 1rem;
        }
        th,
        td {
            vertical-align: top;
            text-align: left;
            padding: 8px;
        }
        th {
            font-weight: a.$regular;
            background-color: a.$primary-color;
            color: white;
        }
        tr {
            border-bottom: 1px solid #ccc;
        }

        tr:nth-child(odd) {
            background-color: rgb(234, 234, 234);
        }
        tr:nth-child(even) {
            background-color: #fff;
        }
    }
    .pagination {
        list-style: none;
        display: flex;
        justify-content: center;
        align-items: center;
        margin: 2rem 0;
        font-size: 1rem;
        position: absolute;
        bottom: 0;
        right: 0;
        left: 0;
    }

    .pagination,
    .page__num,
    .page__next__prev {
        font-family: a.$roboto;
        padding: 5px 10px;
        cursor: pointer;
        border-radius: 3px;
        margin: 2px;
    }

    .pagination .page__num {
        border: 1px solid a.$primary-color;
    }

    .page__active {
        background-color: a.$primary-color;
        color: rgb(255, 255, 255);
        height: 100%;
    }
    .pagination .page__num:hover {
        color: #fff;
        background-color: a.$primary-color;
    }

    .page__disabled__link {
        color: rgb(182, 182, 182);
        cursor: none;
    }
}
.list__icons {
    font-size: 1.2rem;
    cursor: pointer;
}

When I manually change the modal state to true, it renders normally. So I am assuming there's something wrong when the icons are displayed or with the onClick event.

Thank you in advance.


Solution

  • What you are attempting to do is quite odd and not the way you go about this. You should never call ReactDOMServer.renderToString inside a component -- this will create an orphaned "component" that is just a static string, which won't be able to interact with the rest of your tree through standard React constructs.

    Also, setting the innerHTML of anything that's owned by React is generally not allowed. As it's react-owned DOM, and this is an imperative call to change that DOM, then it is effectively mutating outside of Reacts constructs, which just won't work since those mutations will be wiped on rerender.

    The answer you found on SO for this was, unfortunately, very bad advice.

    This is almost certainly the issue. So we need to refactor the hover behavior.

    Two options:

    1. Make it so onMouseEnter, the ID of the row is stored in some state, and then in the render of the rows, for the row that matches that ID -- we render the icons.
    2. Always render the buttons into the DOM for every row, but use CSS to display/hide them on hover.

    Option 2 is almost certainly better, as you don't need to manage the state unnecessarily when it is known to CSS. Additionally, it will feel more performant as you go up and down the rows since you won't have re-renders happening as the user moves the mouse.

    Change the CSS:

    .list__container {
        padding: 2rem;
        hr {
            border: 1px solid a.$hr;
        }
        .list__topSection {
            padding: 3rem 0 1.5rem 0;
            display: flex;
            align-items: center;
            justify-content: space-between;
    
            h3 {
                font-size: 1.5rem;
                font-family: a.$roboto;
                font-weight: a.$medium;
            }
        }
        .list__table {
            font-family: a.$roboto;
            table {
                border-collapse: collapse;
                width: 100%;
    
                font-size: 1rem;
            }
            th,
            td {
                vertical-align: top;
                text-align: left;
                padding: 8px;
            }
            th {
                font-weight: a.$regular;
                background-color: a.$primary-color;
                color: white;
            }
            tr {
                border-bottom: 1px solid #ccc;
            }
    
            tr:nth-child(odd) {
                background-color: rgb(234, 234, 234);
            }
            tr:nth-child(even) {
                background-color: #fff;
            }
    
            tr:hover {
                .list__icons {
                    display: inline-block;
                }
    
                .list__date {
                    display: none;
                }
            }
        }
        .pagination {
            list-style: none;
            display: flex;
            justify-content: center;
            align-items: center;
            margin: 2rem 0;
            font-size: 1rem;
            position: absolute;
            bottom: 0;
            right: 0;
            left: 0;
        }
    
        .pagination,
        .page__num,
        .page__next__prev {
            font-family: a.$roboto;
            padding: 5px 10px;
            cursor: pointer;
            border-radius: 3px;
            margin: 2px;
        }
    
        .pagination .page__num {
            border: 1px solid a.$primary-color;
        }
    
        .page__active {
            background-color: a.$primary-color;
            color: rgb(255, 255, 255);
            height: 100%;
        }
        .pagination .page__num:hover {
            color: #fff;
            background-color: a.$primary-color;
        }
    
        .page__disabled__link {
            color: rgb(182, 182, 182);
            cursor: none;
        }
    }
    .list__icons {
        display: none;
        font-size: 1.2rem;
        cursor: pointer;
    }
    

    Now render it all and apply the right class names:

    const ItemsList = () => {
      const dispatch = useDispatch();
    
      const handleOpenDeleteItemModal = () => {
        dispatch(SET_DELETE_ITEM_MODAL(true));
        dispatch(SET_SIDEBAR(false));
      };
    
      return (
        <div className={styles.list__table}>
          {!isLoading && items.length === 0 ? (
            <p>No items found, please add an item.</p>
          ) : (
            <table>
              <thead>
                <tr>
                  <th>Number</th>
                  <th>Name</th>
                  <th>Category</th>
                  <th>Price</th>
                  <th>Quantity</th>
                  <th>Value</th>
                  <th>Date</th>
                </tr>
              </thead>
              <tbody>
                {currentItems.map((item, index) => {
                  const { _id, name, category, price, quantity, createdAt } = item;
                  return (
                    <tr key={_id}>
                      <td>{index + 1 + "."}</td>
                      <td>{shortenText(name, 15)}</td>
                      <td>{category}</td>
                      <td>
                        {"£"}
                        {price}
                      </td>
                      <td>{quantity}</td>
                      <td>
                        {"£"}
                        {price * quantity}
                      </td>
                      <td>
                        <Moment format="DD/MM/YY" className={styles.list__date}>{createdAt}</Moment>
                        <>
                          <BiMessageSquareEdit className={styles.list__icons} />
                          <BsTrash
                            className={styles.list__icons}
                            onClick={handleOpenDeleteItemModal}
                          />
                        </>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      );
    };