如何在 ReactJS 中没有任何外部库的情况下进行项目进入和项目离开的转换?

how to make transition for item enter and item leave without any external library in ReactJS?


每当用户单击按钮而不使用任何外部库时,我都想制作吐司动画。每当用户单击按钮时,我都会为 toast enter 设置动画。我试图为吐司设置动画以使其离开,但点击 X 按钮后吐司并未从 UI 中移除。

Toast.js

import React from "react";
import { connect } from "react-redux";

import { deleteToast } from "./redux/action/actionCreators";

import "./toast.scss";

const Toast = (props) => {
  const { toasts, deleteToast } = props;

  return (
    <div className="toast-container">
      {toasts.map((toast, index) => {
        return (
          <div className={`toast bg-${toast.type} toast-enter`} key={index}>
            <div className="toast-header">
              <p>{toast.title}</p>
              <button onClick={() => deleteToast(toast.id)}>X</button>
            </div>
            {toast.description && (
              <div className="toast-description">
                <p>{toast.description}</p>
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
};

const mapStateToProps = (state) => ({
  toasts: state.toastReducer.toasts,
});

const mapDispatchToProps = (dispatch) => ({
  deleteToast: (toastId) => dispatch(deleteToast(toastId)),
});

export default connect(mapStateToProps, mapDispatchToProps)(Toast);

Toast.scss

$bg-danger: #dc3545;
$bg-warning: #ffc107;
$bg-info: #0d6efd;
$bg-success: #198754;

.toast-container {
  position: absolute;
  right: 1rem;
  bottom: 1rem;
  z-index: 999;

  .toast {
    padding: 0.5rem 1rem;
    opacity: 0.9;
    color: #fff;
    width: 250px;
    margin-top: 0.5rem;

    &:hover {
      opacity: 1;
    }
  }

  .toast-enter {
    animation: toast-enter-animation 0.3s linear 1;
  }

  .toast-leave {
    animation: toast-leave-animation 0.3s linear 1;
  }

  .toast-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .toast-header button {
    background-color: inherit;
    outline: none;
    border: none;
    color: inherit;

    &:hover {
      cursor: pointer;
    }
  }

  .bg-danger {
    background-color: $bg-danger;
  }

  .bg-warning {
    background-color: $bg-warning;
  }

  .bg-info {
    background-color: $bg-info;
  }

  .bg-success {
    background-color: $bg-success;
  }
}

@mixin keyframes($name) {
  @-webkit-keyframes #{$name} {
    @content;
  }
  @-moz-keyframes #{$name} {
    @content;
  }
  @-ms-keyframes #{$name} {
    @content;
  }
  @keyframes #{$name} {
    @content;
  }
}

@include keyframes(toast-enter-animation) {
  0% {
    transform: translateX(100%);
  }
  100% {
    transform: translateX(0);
  }
}

@include keyframes(toast-leave-animation) {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(100%);
  }
}

App.js

import React from "react";
import { connect } from "react-redux";
import { addToast } from "./Commons/Toast/redux/action/actionCreators";

const App = (props) => {
  const { addToast } = props;

  return (
    <button
      onClick={() =>
        addToast({
          title: "Toast title",
          type: "success",
          description: "Toast description",
        })
      }
    >
      Click
    </button>
  );
};

const mapStateToProps = (state) => ({});

const mapDispatchToProps = (dispatch) => ({
  addToast: (toast) => dispatch(addToast(toast)),
});

export default connect(mapStateToProps, mapDispatchToProps)(App);

我也能够为项目进入和离开进行转换

Toast.js

const Toasts = (props) => {
  const { toasts } = props;

  return (
    <div className="toast-container">
      {toasts.map((toast, index) => (
        <ToastItem toast={toast} key={index} />
      ))}
    </div>
  );
};

Toast.scss

$margin-right: 1rem;
$margin-bottom: 1rem;

.toast-container {
  position: absolute;
  right: $margin-right;
  bottom: $margin-bottom;
  z-index: 999;
}

ToastItem.js

const performAsyncTaskAfter = 300;
const ToastItem = (props) => {
  const [shouldDelete, setShouldDelete] = useState(true);

  const { toast, deleteToast } = props;

  const removeToast = () => {
    setShouldDelete(true);
    setTimeout(() => {
      deleteToast(toast.id);
    }, performAsyncTaskAfter);
  };

  useEffect(() => {
    setTimeout(() => {
      setShouldDelete(false);
    }, performAsyncTaskAfter);
  }, []);

  return (
    <div
      className={`toast bg-${toast.type} ${!shouldDelete ? `toast-enter` : ""}`}
    >
      <div className="toast-header">
        <p>{toast.title}</p>
        <button onClick={removeToast}>X</button>
      </div>
      {toast.description && (
        <div className="toast-description">
          <p>{toast.description}</p>
        </div>
      )}
    </div>
  );
};

ToastItem.scss

$bg-danger: #dc3545;
$bg-warning: #ffc107;
$bg-info: #0d6efd;
$bg-success: #198754;

.toast {
  padding: 0.5rem 1rem;
  opacity: 0.9;
  color: #fff;
  width: 250px;
  margin-top: 0.5rem;
  transform: translateX(calc(100% + 1rem));
  transition: all 0.3s;

  &:hover {
    opacity: 1;
  }
}

.toast-enter {
  transform: translateX(0);
}

.toast-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.toast-header button {
  background-color: inherit;
  outline: none;
  border: none;
  color: inherit;

  &:hover {
    cursor: pointer;
  }
}

.bg-danger {
  background-color: $bg-danger;
}

.bg-warning {
  background-color: $bg-warning;
}

.bg-info {
  background-color: $bg-info;
}

.bg-success {
  background-color: $bg-success;
}