3

I am trying to use Bootstrap to expand and collapse the navbar when the hamburger icon is clicked. The issue is I am using React and I'm pretty sure Bootstrap's DOM manipulation is conflicting with React's DOM manipulation. So I'm trying to pull all the manipulation away from Bootstrap and rely purely on their styling and use React to update the classes instead. That feels like the correct way to me. Now I realize things like React Bootstrap exist but I already have vanilla Bootstrap integrated throughout my project and it appears like React Bootstrap is still on version 4, where I am using 5.

So as far as I can tell, Bootstrap is manipulating the classes on this div: <div id="navbarSupportedContent" style="" class="navbar-collapse collapse">. So it appears that they are adding the class collapsing and removing the class collapse as the menu is expanding or collapsing. And then once it is fully shown, classes become navbar-collapse collapse show and for collapsed: navbar-collapse collapse.

The transition time is set to .35 seconds by Bootstrap. So here is my React code:

const handleNavCollapse = () => {
        if (isNavCollapsed) {
            setNavbarClasses('navbar-collapse collapsing');
            //setTimeout(  <-- I learned this way does not work correctly
            setTimeout(() => 
                setNavbarClasses('navbar-collapse collapse show'),
                350
            );
        } else {
            setNavbarClasses('navbar-collapse collapse');
        }
        setIsNavCollapsed(!isNavCollapsed);
    };

Also, if this helps someone figure out an answer, here is the CSS that Bootstrap is applying with the collapsing class:

.collapsing {
    height: 0;
    overflow: hidden;
    transition: height 0.35s ease;
}

It works halfway in that it shows and hides the navbar menu correctly when clicking the hamburger icon. But there is no transition. I'm not seeing the collapsing class get added when I watch the DOM in the inspector like I do on the Bootstrap site. I only see it add show and remove show. Maybe I'm not doing the setTimeout right.

Update: So I figured out from this SO answer: https://stackoverflow.com/a/42650143/571723 that I was in fact doing the setTimeout wrong. I switched to the arrow function. And now I see the collapsing class getting added right away and then switching to show after the 350ms timeout. Problem is there is still no transition animation, it merely shows the menu after 350ms.

Update 2: I'm getting further with my investigation into how Bootstrap is doing this. I have managed to take a screenshot of the console while Bootstrap was animating the navbar on their site. enter image description here So you can see the height set to a specific number and that changes very quickly (actually that number does not change, it gets set to the 206px and stays there until it is fully opened). And you can also see height: 0 being overridden. Those things are not happening in my site in the inspector.

dmikester1
  • 1,374
  • 11
  • 55
  • 113

1 Answers1

1

First of all let’s start with a small suggestion: do not store classes as state. What you really want is to store some more primitive value that can be used to derive at render time the final UI, classes included.

Something like:

const [isCollapsed, setCollapsed] = React.useState(false);

const className = `navbar-collapse ${isCollapsed ? '' : 'show'}`;

return <div className={className}> … </div>;

You can also use a very common utility such as clsx (usually preferred) or classnames to build it.


Developing a collapsible component would be trivial if browser would interpolate between auto and actual unit values (such as 12px) for CSS properties (such as height) with a transition. But they don’t, you cannot transition from or to auto, but you can interpolate between unit values.

This is indeed what every collapsible (or accordion) UI widget does behind the scenes, it animates between known and explicit unit dimension. Let’s have a look at the original collapsible implementation (which is Vanilla JS, no jQuery) of Bootstrap v5.x.

Let’s rewrite those show() and hide() methods without any noise, such as adding those collapsing classes (they are there exclusively to help with some additional styling). (What follows is pseudo-code and absolutely not React related!)

class Collapsible extends BaseComponent {

  show() {
    this.element.style.height = '0px';
    this.element.style.transition = 'height 300ms';
    this.element.style.height = this.element.scrollHeight + 'px';
  }

  hide() {
    this.element.style.height = this.element.getBoundingClientRect().height + 'px';
    this.element.style.transition = 'height 300ms';
    this.element.style.height = '0px';
  }

}

First we analyze the show() method:

  1. this.element.style.height = '0px';

    This line forces the height of the collapsible element to 0px.

    This is important for the next trick.

  2. this.element.style.transition = 'height 300ms';

    Only now we set the transition configuration. This allows the transition to start only on changes to height that happen after this line. (Actually it’s not that easy and this code should not work as expected, but this is more or less the idea).

  3. this.element.style.height = this.element.scrollHeight + 'px';

    We now use HTMLElement.scrollHeight to retrieve the height the element should have. This works because scrollHeight return the total height available for scrolling (and thus “scroll” and “height”) which for an element with overflow: hidden and height: 0px is the total height of the content.

The hide() method does the same trick but in reverse:

  1. this.element.style.height = this.element.getBoundingClientRect().height + 'px';

    Set the initial height, but this time we use HTMLElement.getBoundingClientRect() to obtain the current dimensions of the element (width and height) since accessing .style.height would give us simply "auto", and we need a unit value (one with “px” in it).

  2. this.element.style.transition = 'height 300ms';

    Enable the transition.

  3. this.element.style.height = '0px';

    Starts the transition by “hiding” the element.


How do we do all this in React? The short answer is “it’s not simple”.

You could implement it purely by state transitions, and would be a pretty good exercise if you were to learn React.

What I can suggest is to actually use some transition/animation library that does it for you, at the cost of adding few KB of dependencies (evaluate it thoroughly though). You need to check if the library supports transition between auto values.

One library that fits the description is react-spring which also has an example more or less identical to what you want to achive: https://codesandbox.io/embed/q3ypxr5yp4

Pier Paolo Ramon
  • 2,780
  • 23
  • 26