简体   繁体   中英

React render previous values during conditional fade out animation

I am using react-transition-group to fade out various components. I'm converting simple conditional renders such as:

{valueToDisplay && <MyComponent {...valueToDisplay} />}

To transitions such as:

<CSSTransition
    in={!!valueToDisplay}
    unmountOnExit
    classNames="fade"
    addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
>
    <MyComponent {...valueToDisplay} />
</CSSTransition>

The issue I'm running into is when the "in" property of the transition becomes false, and the exit transition is running, the child component will now have null prop values. This can cause exceptions or cause the child content to flash and change during the exit. What I would like to see instead is that during the exit transition, the content will remain unchanged.

The first solution I came up with was to make child components to cache previous values of their props, and then use those previous values when their props become null. However I don't like this solution because it forces all components which will be transitioned to introduce new and confusing internal logic.

The second attempt I made was to create a wrapper component which cached the previous value of props.children, and whenever "in" becomes false, renders the cached children instead. This essentially "freezes" the children as they were the last time in was true, and they don't change during the exit transition. (If this solution is the general practice, is there a better way of doing this, perhaps with the useMemo hook?)

For such a common use case of fading content out, this solution doesn't seem very intuitive. I can't help but feeling I'm going about this the wrong way. I can't really find any examples of having to cache/memoize content to keep it displaying during fade outs. It seems like something somewhere has to remember the values to display when performing the exit transition. What am I missing?

Here is a minimal example and working example :

import { useRef, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { CSSTransition } from 'react-transition-group';

const Pet = ({ type, age }) => {
  return (
    <div>
      Your pet {type || 'null'} is age {age || 'null'}
    </div>
  );
};

const Fade = ({ show, children }) => {
  const nodeRef = useRef(null);

  return (
    <CSSTransition
      nodeRef={nodeRef}
      in={show}
      unmountOnExit
      classNames="fade"
      addEndListener={(done) => nodeRef.current.addEventListener("transitionend", done, false)}
    >
      <span ref={nodeRef}>
        {children}
      </span>
    </CSSTransition>
  );
};

const FadeWithMemo = ({ show, children }) => {
  const previousChildren = useRef();

  useEffect(() => {
    previousChildren.current = show ? children : null;
  }, [show, children]);

  return (
    <Fade show={show}>
      {show ? children : previousChildren.current}
    </Fade>
  );
};

const Example = () => {
  const [currentPet, setCurrentPet] = useState(null);

  const getPet = () => {
    return {
      type: (Math.random() > .5) ? 'Cat' : 'Dog',
      age: Math.floor(Math.random() * 15) + 1
    };
  };

  return (
    <>
      <button onClick={() => setCurrentPet(getPet())}>Set</button>
      <button onClick={() => setCurrentPet(null)}>Clear</button>
      
      <div>
        The Problem:
        <Fade show={!!currentPet}>
          <Pet {...currentPet} />
        </Fade>
      </div>
      <div>
        Potential Fix:
        <FadeWithMemo show={!!currentPet}>
          <Pet {...currentPet} />
        </FadeWithMemo>
      </div>
    </>
  );
};

const root = createRoot(document.getElementById('root'));
root.render(<Example />);

You can detach the visible condition from the pet state so that you have more granular control over whether something is visible and what is actually being displayed.

const Example = () => {
  const [currentPet, setCurrentPet] = useState(null);
  const [showPet, setShowPet] = useState(false);

  const getPet = () => {
    return {
      type: (Math.random() > .5) ? 'Cat' : 'Dog',
      age: Math.floor(Math.random() * 15) + 1
    };
  };

  return (
    <>
      <button onClick={() => {
        setCurrentPet(getPet());
        setShowPet(true);
      }}>Set</button>
      <button onClick={() => setShowPet(false)}>Clear</button>
      
      <div>
        <Fade show={showPet}>
          <Pet {...currentPet} />
        </Fade>
      </div>
    </>
  );
};

or you can have the visible be part of the pet state and only set that part to false.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM