Artur Woźniak

Frontend Developer

Jun 03, 2020 in Quick Tips

'Seek first to understand, then to be understood' is one of my life principles. I was more than happy to find my motto in Codetain’s work politics, as well. We’re systematically arranging knowledge sharing sessions...

Quick Tips: using forwardRef to measure element height dynamically

Hello there :)
Last time, I promised to explain why I used React.forwardref() in the Navigation component.

The answer is very simple: I needed to know the height of Navigation to move all other elements from it. Otherwise, fixed Navigation will cover the next element.
Of course, we can do that manually — inspecting our navigation in a browser.

Here, I’d like to mention the CSS box-sizing property.
With CSS box-sizing, we can define how the width and height of an element will be calculated: should they include padding and borders, or not.

Let’s visualise this definition:
(I’m inspecting our previous code exemple - https://codetain.com/blog/quick-tips-add-and-remove-active-style)

Our navigation has now the ‘box-sizing’ property set to ‘border-box’.
Now, let’s go from ‘Styles’ to ‘Computed’ and check the navigation height:

As you can see, the height is equal to 89px. It means that using box-size: border-box height is calculated like this: actual height = height + padding + border (as the definition says).In the above example height = 19px, padding-top = 35px, padding-bottom = 35px, border = 0 as result actual height = 19 + 35 + 35.

Now, let’s disable our box-sizing property to see how the navigation height will change.

As a result we will see:

Navigation is now 19px high.

It’s crucial to set box-sizing to border-box property for all HTML elements. To do that, set in the main css this selector:
*{ box-sizing: border-box}

Having the navigation height value we can set margin-top as navigation height + gap for next HTML element to push it from the fixed navigation, margin-top = calc(89px + 20px).

A disadvantage of this approach is that when you’ll add, e.g., border: 2px solid red, you’ll have to recalculate your margin by adding 4px. Otherwise, the distance between navigation and next HTML element will shrink by 4px.

To avoid that you can use a simple combination of React.createRef(), React.forwardRef(), and useEffect() hook. Using the combination will automatise the calculation of actual height and setting the margin-top.

‘Show me!’

Here you go :)

App component:

import React, { useEffect, useState, useRef } from "react";
import { render } from "react-dom";
import Navigation from "./components/Navigation";
import "./style.css";

const App = () => {
  const [activeNavOption, setActiveNavOption] = useState("home");
  const [navigationHeight, setNavigationHeight] = useState(undefined);
  const navigation = React.createRef();

  useEffect(() => {
    setNavigationHeight(navigation.current.offsetHeight);
  }, []);

  const handleNavOptionClick = event => {
    const navOptionName = event.target.getAttribute("data-name");
    setActiveNavOption(navOptionName);
  };

  return (
    <>
      <Navigation
        ref={navigation}
        currentOption={activeNavOption}
        handleNavOptionClick={handleNavOptionClick}
      />
      <div style={{ marginTop: `${navigationHeight + 20}px` }}>
	<p>Hello! I’m in container under fixed navigation</p>
      </div>
    </>
  );
};

render(<App />document.getElementById("root"));

What has changed compared to last quick tip? Our App component has now:

  • useEffect() hook  — once component is mounted, it will catch navigation height (For clarification, the offsetHeight property returns the viewable height of an element in pixels, including padding, border and scrollbar, but not the margin.)
  • navigationHeight state
  • div element with marginTop which is a sum of navigationHeight and custom 20px gap
  • paragraph inside the div element

Navigation component:

import React, { forwardRef } from "react";

const styles = {
  nav: {
    position: "fixed",
    top: 0,
    right: 0,
    left: 0,
    backgroundColor: "AliceBlue",
    padding: "35px",
    color: "DarkOrange"
  },
  list: {
    margin: 0,
    padding: 0,
    display: "flex",
    justifyContent: "flex-end"
  },
  listItem: {
    listStyleType: "none",
    marginRight: "20px",
    textTransform: "uppercase",
    cursor: "pointer"
  }
};

const activeStyle = {
  ...styles.listItem,
  color: "black",
  borderBottom: "1px solid"
};

const navigationOptions = [
  {
    id: 1,
    option: "home"
  },
  {
    id: 2,
    option: "about"
  },
  {
    id: 3,
    option: "contact"
  }
];

const Navigation = forwardRef(
  ({ currentOption, handleNavOptionClick }, ref) => {
    const { nav, list, listItem } = styles;

    return (
      <nav ref={ref} style={nav}>
        <ul style={list}>
          {navigationOptions &&
            navigationOptions.map(({ id, option }) => {
              return (
                <li
                  key={option}
                  style={currentOption === option ? activeStyle : listItem}
                  data-name={option}
                  onClick={handleNavOptionClick}
                >
                  {option}
                </li>
              );
            })}
        </ul>
      </nav>
    );
  }
);

export default Navigation;

The navigation component is exactly the same as in the previous exemple.

Result of our code:

In this quick-tip, you could see how to use React.createRef(), React.forwardRef() and useEffect() hook to check the height element and use this value to move another element from it.

Furthermore, you know now that it’s possible to automatise the response from the element for internal changes — along with the internal changes modification, the height element changes proportionally to it.

In the next quick-tip, I’d like to present a way to display different content after clicking the navigation option.

If you’re curious, stay tuned! :)

Back to top