Search code examples
reactjsreact-hookscss-transitionsreact-transition-groupreact-portal

CSSTransition not working with React Portals


On this StackBlitz: https://stackblitz.com/edit/csstransition-test

I have a project that open a green div with a zoom-in effect:

/src/App.js

import React, { useState } from 'react';
import './style.css';
import Modal from './Modal';

export default function App() {

  const [ isOpen, setIsOpen ] = useState(false);

  const handleClick = () => {
    setIsOpen(!isOpen);
  };

  return (
    <div>
      <h1>Hello StackBlitz!</h1>
      <p>Start editing to see some magic happen :)</p>
      <button onClick={handleClick}>Click Me</button>
      <br /><br />
      <Modal isOpen={isOpen}>
        <div>Hello World</div>
      </Modal>
    </div>
  );
}

/src/Portal.js

import { Component } from 'react';
import ReactDOM from 'react-dom';

const portal = document.getElementById('portal');

export default class Portal extends Component {
  render() {
    return ReactDOM.createPortal(this.props.children, portal);
  }
}

/src/Modal.jsx

import React, { useRef } from "react";
import { CSSTransition } from "react-transition-group";
import Portal from "./Portal";

export default function Modal({ isOpen, children }) {

  const container = useRef(null);

  return (
    <CSSTransition
      nodeRef={container}
      in={isOpen}
      timeout={300}
      classNames="modal"
      unmountOnExit
      onEnter={ () => console.log('entered') }
      onExited={ () => console.log('exited') }
    >
      <div ref={container} style={{
        display: 'inline-block',
        backgroundColor: '#0f0',
        padding: '50px',
      }}>
        {children}
      </div>
    </CSSTransition>
  );
}

/src/style.css

h1,
p {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
    Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

.modal-enter {
  transform: scale(0);
}
.modal-enter-active {
  transform: scale(1);
  transition: transform 300ms;
}
.modal-exit {
  transform: scale(1);
}
.modal-exit-active {
  transform: scale(0);
  transition: transform 300ms;
}

My problem is: I need to make it work with Portals.

On file: /src/Modal.jsx, if I replace the div inside: CSSTransition with: Portal then I get errors.

This is the code with the change:

/src/Modal.jsx

import React, { useRef } from "react";
import { CSSTransition } from "react-transition-group";
import Portal from "./Portal";

export default function Modal({ isOpen, children }) {

  const container = useRef(null);

  return (
    <CSSTransition
      nodeRef={container}
      in={isOpen}
      timeout={300}
      classNames="modal"
      unmountOnExit
      onEnter={ () => console.log('entered') }
      onExited={ () => console.log('exited') }
    >
      <Portal ref={container} style={{
        display: 'inline-block',
        backgroundColor: '#0f0',
        padding: '50px',
      }}>
        {children}
      </Portal>
    </CSSTransition>
  );
}

Please try the StackBlitz: https://stackblitz.com/edit/csstransition-test and you are also welcomed to post your own forked StackBlitz with the fixes.

Thanks!


Solution

  • I think this is happing because CSSTransition component is being created in root element. And the model is created in your portal element. This CSSTransition component is not wrapping anything.

    You should instead wrap the whole Model with the portal, like this:

    <Portal>
      <CSSTransition
        nodeRef={container}
        in={isOpen}
        timeout={300}
        classNames="modal"
        unmountOnExit
        onEnter={ () => console.log('entered') }
        onExited={ () => console.log('exited') }
        >
        <div ref={container} style={{
          display: 'inline-block',
          backgroundColor: '#0f0',
          padding: '50px',
        }}>
          {children}
        </div>
      </CSSTransition>
    </Portal>
    

    I hope this helps.