Animate height: auto with removing content from DOM in React.js
Tags: react, css, javascript, html
Published: 2024-04-16 17:47:45

During a recent project, I encountered a seemingly straightforward task: animating content expansion upon clicking a title.

However, implementing this in React posed challenges, especially since the component had to accommodate dynamic content with unknown heights.

## Problem №1

Unfortunately, we still can't just write something like
```css
.content {
  height: 0;
  transition: height .3s ease;
}

.content.expanded {
  height: auto;
}
``` 
because it won't work.

Of course, we can implement this using JavaScript, but I decided not to retreat and still try to find ways to use only CSS for animation.

![success kid meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fu98h5443klviy29a0nz.png)

And I was lucky, because I was found examples of animation implementation using only css grid. [Here](https://keithjgrant.com/posts/2023/04/transitioning-to-height-auto/) is one of these wonderful articles.

## Solution №1

Great, we have an example of animation, so all we have to do is implement it in react. 
Simple implementation will look something like this:
```css
.content {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.5s ease-out;
}

.content.expanded {
  grid-template-rows: 1fr;
}

.inner {
  overflow: hidden;
}
```
```tsx
const Expandable = ({
  children,
  defaultExpaded = false,
  title,
}: {
  children: ReactNode;
  title: ReactNode;
  defaultExpaded?: boolean;
}) => {
  const [expanded, setExpanded] = useState(defaultExpaded);

  return (
    <>
      <button onClick={() => setExpanded(!expanded)}>{title}</button>
      <div className={`content ${expanded ? 'expanded' : ''}`}>
        <div className="inner">{children}</div>
      </div>
    </>
  );
};

function App() {
  return (
    <Expandable title="Logo" defaultExpaded>
      <img src={reactLogo} className="logo react" alt="React logo" />
    </Expandable>
  );
}
```
So everything works as expected so we can submit the task for code review, drink a cup of tea and take on a new task.

![cup of tea with KEEP CALM AND DRINK TEA text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lo74gu6qgz9mkfkykp3n.png)

After some time during the code review, I received a question: 

> Can we make it so that if the content is collapsed, then we do not render the DOM that is hidden?

Good question, so let's think about it

## Problem №2
It would seem that we could simply add a check and render `children` only if `expanded === true`:
```tsx
<>
  <button onClick={() => setExpanded(!expanded)}>{title}</button>
  <div className={`content ${expanded ? 'expanded' : ''}`}>
    {/* Add check for expanded */}
    <div className="inner">{expanded && children}</div>
  </div>
</>
```
but this won't work, because when`expanded === false` we will not have any content, which means that the animation will not happen.

## Solution №2
So, we know that the problem is that during animation we have no content. It means we need to come up with something that will hide the content after our animation ends.

![Thinking meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/meouyn4f6auxjfwrzwef.png)

Fortunately, we have an event that will be called exactly after the animation ends. Because we use css tranision property for animation, we can simply add an `onTransitionEnd` event handler to the animated `div` and we alsow will need an additional variable that will control the visibility.

So let's put this functionality into a custom hook for not to write logic inside the component:
```tsx
const useExpand = (defaultExpanded: boolean) => {
  const [visible, setVisible] = useState(defaultExpanded);
  const [expanded, setExpanded] = useState(defaultExpanded);

  const toggle = useCallback(() => {
    if (!expanded) {
      setVisible(true);
      setExpanded(true);
    } else {
      setExpanded(false);
    }
  }, [visible, expanded]);

  const onToggled = useCallback(() => {
    if (visible && !expanded) {
      setVisible(false);
    }
  }, [expanded, visible]);

  return { toggle, expanded, visible, onToggled };
};
```
Let's see on `useExpand` hook closely.
We declared an additional variable `visible`, which will be responsible for whether to delete the DOM or not. To control toggle we will use the `toggle` function, and the animation end event will be handled by the `onToggled` function.

Now we can use this hook in our `Expandable` component
```tsx
const Expandable = ({
  children,
  defaultExpaded = false,
  title,
}: {
  children: ReactNode;
  title: ReactNode;
  defaultExpaded?: boolean;
}) => {
  const content = useExpand(defaultExpaded);

  return (
    <>
      <button onClick={content.toggle}>{title}</button>
      <div
        className={`content ${content.expanded ? 'expanded' : ''}`}
        onTransitionEnd={content.onToggled}
      >
        <div className="inner">{content.visible && children}</div>
      </div>
    </>
  );
};
```
> Note: Because in this example the animation occurs through css transition, we attach the `onToggled` function to the `onTransitionEnd` event. If we used css animation the event would be `onAnimationEnd`.

Here is live example of code
{% embed https://stackblitz.com/edit/vitejs-vite-uczfs2?embed=1&file=src%2FApp.tsx %}

## Recap
So, in this article I wanted to show how you can implement height animation and hide content from the DOM when the content is collapsed. 
We wrote a hook that can be used in any other component, keeping in mind the necessary markup and handlers for the corresponding events

*Special thanks to my colleague Evgeniy for his invaluable contributions to improving readability and code quality*