Create a React SlideToggle component with hooks and react-spring

ReactAnimation

Everybody who has been writing Javascript for more than a few years must have come across jQuery’s slideDown() and slideToggle() methods. You can say about jQuery what you want, but these methods were very easy and helpful. When I first started using a component based framework such as React I really missed an easy way to mount and unmount content with a fancy transition. For quite some time I have been dependent on the Transition component from React Transition Group (opens in a new tab). It gets the job done, but in an era of Hooks and spring physics it’s renderProp API feels a bit outdated and the animations a bit sluggish.

Because we at CLEVER°FRANKE (opens in a new tab) have been a heavy users of Hooks (opens in a new tab) since React 16.8 came out and since React Spring (opens in a new tab) now added a very easy Hooks interface (opens in a new tab) in v7, I decided to combine the best of both worlds to create a reusable SlideToggle component. Below I will describe my process and choices so it may hopefully help you or others.

The first thing we need to do is make sure we are running a React version that supports hooks and install react-spring via:

npm install --save react-spring

and create a component that will serve as the wrapper element.

const SlideToggleContent = ({ isVisible, children, forceSlideIn }) => {
  //...
};
 
SlideToggleContent.defaultProps = {
  forceSlideIn: false,
};
 
SlideToggleContent.propTypes = {
  isVisible: bool.isRequired,
  forceSlideIn: bool,
  children: node.isRequired,
};

The component will have three props:

  • isVisible which will make sure the children will be mounted and slided down.
  • forceSlideIn will be an optional prop that makes sure the component slides down on it’s initial render.
  • children will contain the children elements.

Next we will need to add the actual HTML elements and add references to them so that React can interact with them. The new Hooks API provides the useRef hook that “returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component” (See the React docs (opens in a new tab)).

We need a little bit of styling to prevent margins of the children elements from overflowing the container. I’m using Styled Components (opens in a new tab) but you can of course choose any other styling method.

const Inner = styled.div`
    &:before,
    &:after {
        content: "",
        display: table
    }
`;
 
const SlideToggleContent = ({ isVisible, children, forceSlideIn }) => {
  const containerRef = useRef(null);
  const innerRef = useRef(null);
 
  return (
    <div ref={containerRef}>
      <Inner ref={innerRef}>{children}</Inner>
    </div>
  );
};

Now it is time for the React Spring magic to come into play. We will be using the useTransition hook (opens in a new tab) since we are mounting and unmounting the children elements and this hook provides from, enter and leave properties just like the Transition component from React Transition Group. These properties allow you to add basic styling objects or more advanced async functions that can chain multiple style changes. The latter is very useful in our case because we want to calculate the proper final element height and make sure that after that the css height property is set to auto. Furthermore we want to control the overflow property and set it to visible whenever the animation changes.

The useTransition(items, key, config) hook can mount multiple instances of children at once, for example when the component is toggled before the animation is ended. This is something we want to prevent because we don’t want duplicates of our content showing up. React Spring provides the unique: true property which will make sure items with the same key will be reused and never rendered more than once. In our case we can omit the key parameter and set it to null because we will only animate one single element. React Spring will make sure the elements receives a proper key.

useTransition will return an Array of transitionable element objects each containing an item, props and key that contain respectively a reference to the actual element, the styling properties and the unique key. The styling properties can be applied on an animated component provided by React Spring which will make sure the styling props are transformed into actual animations.

const visibleStyle = { height: "auto", opacity: 1, overflow: "visible" };
const hiddenStyle = { opacity: 0, height: 0, overflow: "hidden" };
 
function getElementHeight(ref) {
  return ref.current ? ref.current.getBoundingClientRect().height : 0;
}
 
const SlideToggleContent = ({ isVisible, children, forceSlideIn }) => {
  // ...
 
  const transitions = useTransition(isVisible, null, {
    enter: () => async (next) => {
      const height = getElementHeight(innerRef);
 
      if (height) {
        await next({ height, opacity: 1, overflow: "hidden" });
        await next(visibleStyle);
      }
    },
    leave: () => async (next) => {
      const height = getElementHeight(innerRef);
 
      if (height) {
        await next({ height, overflow: "hidden" });
        await next(hiddenStyle);
      }
    },
    from: hiddenStyle,
    unique: true,
  });
 
  return transitions.map(({ item: show, props: springProps, key }) => {
    if (show) {
      return (
        <animated.div ref={containerRef} key={key} style={springProps}>
          <Inner ref={innerRef}>{children}</Inner>
        </animated.div>
      );
    }
 
    return null;
  });
};

Almost there! We now have a working SlideToggle component. It is however missing one important feature. At this point the component will always start from height: 0; also when isVisible was true on the components initial render. So we need to make sure that the from property changes based on the initial value of isVisible. In order to remember that initial value we can use the useRef hook again because as said above that will persist the value throughout the lifetime of the component.

const isVisibleOnMount = useRef(isVisible && !forceSlideIn);

And change the from property to:

from: isVisibleOnMount.current ? visibleStyle : hiddenStyle;

You can find a working demo of this component here (opens in a new tab). Feel free to reuse it!

With a little bit of code we’ve been able to create a reusable and easily customisable SlideToggle component. Adding React Spring to your bundle for only this use case may be a bit overkill but I can assure you that once it’s added to your bundle and you start playing with it, you will certainly find more UI elements that can be brought to life with spring animations!