Search code examples
javascriptreactjsreduxcss-transitions

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



I want to animate toast whenever a user clicks on the button without using any external libraries. I have animated the toast enter whenever a user clicks on the button. I tried to animate the toast to leave, but the toast is not getting removed from the UI on clicking the X button.

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);


Solution

  • I was able to able to make transition for item enter and leave as well

    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;
    }