1

I am trying to build a barebones css transition wrapper in React, where a boolean property controls an HTML class that toggles css properties that are set to transition. For the use case in question, we also want the component to be unmounted (return null) before the entrance transition and after the exit transition.

To do this, I use two boolean state variables: one that controls the mounting and one that control the HTML class. When props.in goes from false to true, I set mounted to true. Now the trick: if the class is set immediate to "in" when it's first rendered, the transition does not occur. We need the component to be rendered with class "out" first and then change the class to "in".

A setTimeout works but is pretty arbitrary and not strictly tied to the React lifecycle. I've found that even a timeout of 10ms can sometimes fail to produce the effect. It's a crapshoot.

I had thought that using useEffect with mounted as the dependency would work because the component would be rendered and the effect would occur after:

  useEffect(if (mounted) { () => setClass("in"); }, [mounted]);

(see full code in context below)

but this fails to produce the transition. I believe this is because React batches operations and chooses when to render to the real DOM, and most of the time doesn't do so until after the effect has also occurred.

How can I guarantee that my class value is change only after, but immediately after, the component is rendered after mounted gets set to true?

Simplified React component:

function Transition(props) {
  const [inStyle, setInStyle] = useState(props.in);
  const [mounted, setMounted] = useState(props.in);

  function transitionAfterMount() {
    // // This can work if React happens to render after mounted get set but before
    // // the effect; but this is inconsistent. How to wait until after render?
    setInStyle(true);

    // // this works, but is arbitrary, pits UI delay against robustness, and is not
    // // tied to the React lifecycle
    // setTimeout(() => setInStyle(true), 35);
  }

  function unmountAfterTransition() {
    setTimeout(() => setMounted(false), props.duration);
  }

  // mount on props.in, or start exit transition on !props.in
  useEffect(() => {
    props.in ? setMounted(true) : setInStyle(false);
  }, [props.in]);

  // initiate transition after mount
  useEffect(() => {
    if (mounted) { transitionAfterMount(); }
  }, [mounted]);

  // unmount after transition
  useEffect(() => {
    if (!props.in) { unmountAfterTransition(); }
  }, [props.in]);

  if (!mounted) { return false; }

  return (
    <div className={"transition " + inStyle ? "in" : "out"}>
      {props.children}
    </div>
  )
}

Example styles:

.in: {
  opacity: 1;
}
.out: {
  opacity: 0;
}
.transition {
  transition-property: opacity;
  transition-duration: 1s;
}

And usage

function Main() {
  const [show, setShow] = useState(false);
  return (
    <>
      <div onClick={() => setShow(!show)}>Toggle</div>
      <Transition in={show} duration={1000}>
        Hello, world.
      </Transition>
      <div>This helps us see when the above component is unmounted</div>
    </>
  );
}

1 Answer 1

2

Found the solution looking outside of React. Using window.requestAnimationFrame allows an action to be take after the next DOM paint.

  function transitionAfterMount() {
    // hack: setTimeout(() => setInStyle(true), 35);
    // not hack:
    window.requestAnimationFrame(() => setInStyle(true));
  }
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.